ccmanager 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,11 @@
1
- # CCManager - AI Code Assistant Session Manager
2
-
3
- CCManager is a TUI application for managing multiple AI coding assistant sessions (Claude Code, Gemini CLI) across Git worktrees and projects.
1
+ # CCManager - AI Code Agent Session Manager
4
2
 
3
+ [![Mentioned in Awesome Gemini CLI](https://awesome.re/mentioned-badge.svg)](https://github.com/Piebald-AI/awesome-gemini-cli)
5
4
 
5
+ CCManager is a CLI application for managing multiple AI coding assistant sessions (Claude Code, Gemini CLI, Codex CLI) across Git worktrees and projects.
6
6
 
7
7
  https://github.com/user-attachments/assets/15914a88-e288-4ac9-94d5-8127f2e19dbf
8
8
 
9
-
10
-
11
9
  ## Features
12
10
 
13
11
  - Run multiple AI assistant sessions in parallel across different Git worktrees
@@ -0,0 +1,2 @@
1
+ export declare const STATE_PERSISTENCE_DURATION_MS = 200;
2
+ export declare const STATE_CHECK_INTERVAL_MS = 100;
@@ -0,0 +1,4 @@
1
+ // Duration in milliseconds that a detected state must persist before being confirmed
2
+ export const STATE_PERSISTENCE_DURATION_MS = 200;
3
+ // Check interval for state detection in milliseconds
4
+ export const STATE_CHECK_INTERVAL_MS = 100;
@@ -6,6 +6,7 @@ import { promisify } from 'util';
6
6
  import { configurationManager } from './configurationManager.js';
7
7
  import { executeStatusHook } from '../utils/hookExecutor.js';
8
8
  import { createStateDetector } from './stateDetector.js';
9
+ import { STATE_PERSISTENCE_DURATION_MS, STATE_CHECK_INTERVAL_MS, } from '../constants/statePersistence.js';
9
10
  const { Terminal } = pkg;
10
11
  const execAsync = promisify(exec);
11
12
  export class SessionManager extends EventEmitter {
@@ -23,7 +24,7 @@ export class SessionManager extends EventEmitter {
23
24
  // Create a detector based on the session's detection strategy
24
25
  const strategy = session.detectionStrategy || 'claude';
25
26
  const detector = createStateDetector(strategy);
26
- return detector.detectState(session.terminal);
27
+ return detector.detectState(session.terminal, session.state);
27
28
  }
28
29
  constructor() {
29
30
  super();
@@ -71,10 +72,13 @@ export class SessionManager extends EventEmitter {
71
72
  lastActivity: new Date(),
72
73
  isActive: false,
73
74
  terminal,
75
+ stateCheckInterval: undefined, // Will be set in setupBackgroundHandler
74
76
  isPrimaryCommand: options.isPrimaryCommand ?? true,
75
77
  commandConfig,
76
78
  detectionStrategy: options.detectionStrategy ?? 'claude',
77
- devcontainerConfig: options.devcontainerConfig,
79
+ devcontainerConfig: options.devcontainerConfig ?? undefined,
80
+ pendingState: undefined,
81
+ pendingStateStart: undefined,
78
82
  };
79
83
  // Set up persistent background data handler for state detection
80
84
  this.setupBackgroundHandler(session);
@@ -188,16 +192,39 @@ export class SessionManager extends EventEmitter {
188
192
  setupBackgroundHandler(session) {
189
193
  // Setup data handler
190
194
  this.setupDataHandler(session);
195
+ // Set up interval-based state detection with persistence
191
196
  session.stateCheckInterval = setInterval(() => {
192
197
  const oldState = session.state;
193
- const newState = this.detectTerminalState(session);
194
- if (newState !== oldState) {
195
- session.state = newState;
196
- // Execute status hook asynchronously (non-blocking)
197
- void executeStatusHook(oldState, newState, session);
198
- this.emit('sessionStateChanged', session);
198
+ const detectedState = this.detectTerminalState(session);
199
+ const now = Date.now();
200
+ // If detected state is different from current state
201
+ if (detectedState !== oldState) {
202
+ // If this is a new pending state or the pending state changed
203
+ if (session.pendingState !== detectedState) {
204
+ session.pendingState = detectedState;
205
+ session.pendingStateStart = now;
206
+ }
207
+ else if (session.pendingState !== undefined &&
208
+ session.pendingStateStart !== undefined) {
209
+ // Check if the pending state has persisted long enough
210
+ const duration = now - session.pendingStateStart;
211
+ if (duration >= STATE_PERSISTENCE_DURATION_MS) {
212
+ // Confirm the state change
213
+ session.state = detectedState;
214
+ session.pendingState = undefined;
215
+ session.pendingStateStart = undefined;
216
+ // Execute status hook asynchronously (non-blocking)
217
+ void executeStatusHook(oldState, detectedState, session);
218
+ this.emit('sessionStateChanged', session);
219
+ }
220
+ }
221
+ }
222
+ else {
223
+ // Detected state matches current state, clear any pending state
224
+ session.pendingState = undefined;
225
+ session.pendingStateStart = undefined;
199
226
  }
200
- }, 100); // Check every 100ms
227
+ }, STATE_CHECK_INTERVAL_MS);
201
228
  // Setup exit handler
202
229
  this.setupExitHandler(session);
203
230
  }
@@ -205,7 +232,11 @@ export class SessionManager extends EventEmitter {
205
232
  // Clear the state check interval
206
233
  if (session.stateCheckInterval) {
207
234
  clearInterval(session.stateCheckInterval);
235
+ session.stateCheckInterval = undefined;
208
236
  }
237
+ // Clear any pending state
238
+ session.pendingState = undefined;
239
+ session.pendingStateStart = undefined;
209
240
  // Update state to idle before destroying
210
241
  session.state = 'idle';
211
242
  this.emit('sessionStateChanged', session);
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ import { SessionManager } from './sessionManager.js';
3
+ import { spawn } from 'node-pty';
4
+ import { EventEmitter } from 'events';
5
+ import { STATE_PERSISTENCE_DURATION_MS, STATE_CHECK_INTERVAL_MS, } from '../constants/statePersistence.js';
6
+ vi.mock('node-pty');
7
+ vi.mock('./configurationManager.js', () => ({
8
+ configurationManager: {
9
+ getConfig: vi.fn().mockReturnValue({
10
+ commands: [
11
+ {
12
+ id: 'test',
13
+ name: 'Test',
14
+ command: 'test',
15
+ args: [],
16
+ },
17
+ ],
18
+ defaultCommandId: 'test',
19
+ }),
20
+ getPresetById: vi.fn().mockReturnValue({
21
+ id: 'test',
22
+ name: 'Test',
23
+ command: 'test',
24
+ args: [],
25
+ }),
26
+ getDefaultPreset: vi.fn().mockReturnValue({
27
+ id: 'test',
28
+ name: 'Test',
29
+ command: 'test',
30
+ args: [],
31
+ }),
32
+ getHooks: vi.fn().mockReturnValue({}),
33
+ getStatusHooks: vi.fn().mockReturnValue({}),
34
+ },
35
+ }));
36
+ describe('SessionManager - State Persistence', () => {
37
+ let sessionManager;
38
+ let mockPtyInstances;
39
+ let eventEmitters;
40
+ beforeEach(() => {
41
+ vi.useFakeTimers();
42
+ sessionManager = new SessionManager();
43
+ mockPtyInstances = new Map();
44
+ eventEmitters = new Map();
45
+ // Create mock PTY process factory
46
+ spawn.mockImplementation((command, args, options) => {
47
+ const path = options.cwd;
48
+ const eventEmitter = new EventEmitter();
49
+ eventEmitters.set(path, eventEmitter);
50
+ const mockPty = {
51
+ onData: vi.fn((callback) => {
52
+ eventEmitter.on('data', callback);
53
+ return { dispose: vi.fn() };
54
+ }),
55
+ onExit: vi.fn((callback) => {
56
+ eventEmitter.on('exit', callback);
57
+ return { dispose: vi.fn() };
58
+ }),
59
+ write: vi.fn(),
60
+ resize: vi.fn(),
61
+ kill: vi.fn(),
62
+ process: 'test',
63
+ pid: 12345 + mockPtyInstances.size,
64
+ };
65
+ mockPtyInstances.set(path, mockPty);
66
+ return mockPty;
67
+ });
68
+ });
69
+ afterEach(() => {
70
+ vi.clearAllTimers();
71
+ vi.useRealTimers();
72
+ vi.clearAllMocks();
73
+ });
74
+ it('should not change state immediately when detected state changes', async () => {
75
+ const session = await sessionManager.createSessionWithPreset('/test/path');
76
+ const eventEmitter = eventEmitters.get('/test/path');
77
+ // Initial state should be busy
78
+ expect(session.state).toBe('busy');
79
+ // Simulate output that would trigger idle state
80
+ eventEmitter.emit('data', 'Some output without busy indicators');
81
+ // Advance time less than persistence duration
82
+ vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS * 2);
83
+ // State should still be busy, but pending state should be set
84
+ expect(session.state).toBe('busy');
85
+ expect(session.pendingState).toBe('idle');
86
+ expect(session.pendingStateStart).toBeDefined();
87
+ });
88
+ it('should change state after persistence duration is met', async () => {
89
+ const session = await sessionManager.createSessionWithPreset('/test/path');
90
+ const eventEmitter = eventEmitters.get('/test/path');
91
+ const stateChangeHandler = vi.fn();
92
+ sessionManager.on('sessionStateChanged', stateChangeHandler);
93
+ // Initial state should be busy
94
+ expect(session.state).toBe('busy');
95
+ // Simulate output that would trigger idle state
96
+ eventEmitter.emit('data', 'Some output without busy indicators');
97
+ // Advance time less than persistence duration
98
+ vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS * 2);
99
+ expect(session.state).toBe('busy');
100
+ expect(stateChangeHandler).not.toHaveBeenCalled();
101
+ // Advance time to exceed persistence duration
102
+ vi.advanceTimersByTime(STATE_PERSISTENCE_DURATION_MS);
103
+ // State should now be changed
104
+ expect(session.state).toBe('idle');
105
+ expect(session.pendingState).toBeUndefined();
106
+ expect(session.pendingStateStart).toBeUndefined();
107
+ expect(stateChangeHandler).toHaveBeenCalledWith(session);
108
+ });
109
+ it('should cancel pending state if detected state changes again before persistence', async () => {
110
+ const session = await sessionManager.createSessionWithPreset('/test/path');
111
+ const eventEmitter = eventEmitters.get('/test/path');
112
+ // Initial state should be busy
113
+ expect(session.state).toBe('busy');
114
+ // Simulate output that would trigger idle state
115
+ eventEmitter.emit('data', 'Some output without busy indicators');
116
+ // Advance time less than persistence duration
117
+ vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS * 2);
118
+ expect(session.pendingState).toBe('idle');
119
+ // Simulate output that would trigger waiting_input state
120
+ eventEmitter.emit('data', '│ Do you want to continue?');
121
+ // Advance time to trigger another check
122
+ vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS);
123
+ // Pending state should now be waiting_input, not idle
124
+ expect(session.state).toBe('busy'); // Still original state
125
+ expect(session.pendingState).toBe('waiting_input');
126
+ });
127
+ it('should clear pending state if detected state returns to current state', async () => {
128
+ const session = await sessionManager.createSessionWithPreset('/test/path');
129
+ const eventEmitter = eventEmitters.get('/test/path');
130
+ // Initial state should be busy
131
+ expect(session.state).toBe('busy');
132
+ // Simulate output that would trigger idle state
133
+ eventEmitter.emit('data', 'Some output without busy indicators');
134
+ // Advance time less than persistence duration
135
+ vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS * 2);
136
+ expect(session.pendingState).toBe('idle');
137
+ expect(session.pendingStateStart).toBeDefined();
138
+ // Simulate output that would trigger busy state again (back to original)
139
+ eventEmitter.emit('data', 'ESC to interrupt');
140
+ // Advance time to trigger another check
141
+ vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS);
142
+ // Pending state should be cleared
143
+ expect(session.state).toBe('busy');
144
+ expect(session.pendingState).toBeUndefined();
145
+ expect(session.pendingStateStart).toBeUndefined();
146
+ });
147
+ it('should not confirm state changes that do not persist long enough', async () => {
148
+ const session = await sessionManager.createSessionWithPreset('/test/path');
149
+ const eventEmitter = eventEmitters.get('/test/path');
150
+ const stateChangeHandler = vi.fn();
151
+ sessionManager.on('sessionStateChanged', stateChangeHandler);
152
+ // Initial state should be busy
153
+ expect(session.state).toBe('busy');
154
+ // Try to change to idle
155
+ eventEmitter.emit('data', 'Some idle output\n');
156
+ // Wait for detection but not full persistence (less than 200ms)
157
+ vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS); // 100ms
158
+ // Should have pending state but not confirmed
159
+ expect(session.state).toBe('busy');
160
+ expect(session.pendingState).toBe('idle');
161
+ // Now change to a different state before idle persists
162
+ // Clear terminal first and add waiting prompt
163
+ eventEmitter.emit('data', '\x1b[2J\x1b[H│ Do you want to continue?\n');
164
+ // Advance time to detect new state but still less than persistence duration from first change
165
+ vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS); // Another 100ms, total 200ms exactly at threshold
166
+ // Pending state should have changed to waiting_input
167
+ expect(session.state).toBe('busy'); // Still original state
168
+ expect(session.pendingState).toBe('waiting_input');
169
+ // Since states kept changing before persisting, no state change should have been confirmed
170
+ expect(stateChangeHandler).not.toHaveBeenCalled();
171
+ });
172
+ it('should properly clean up pending state when session is destroyed', async () => {
173
+ const session = await sessionManager.createSessionWithPreset('/test/path');
174
+ const eventEmitter = eventEmitters.get('/test/path');
175
+ // Simulate output that would trigger idle state
176
+ eventEmitter.emit('data', 'Some output without busy indicators');
177
+ // Advance time less than persistence duration
178
+ vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS * 2);
179
+ expect(session.pendingState).toBe('idle');
180
+ expect(session.pendingStateStart).toBeDefined();
181
+ // Destroy the session
182
+ sessionManager.destroySession('/test/path');
183
+ // Check that pending state is cleared
184
+ const destroyedSession = sessionManager.getSession('/test/path');
185
+ expect(destroyedSession).toBeUndefined();
186
+ });
187
+ it('should handle multiple sessions with independent state persistence', async () => {
188
+ const session1 = await sessionManager.createSessionWithPreset('/test/path1');
189
+ const session2 = await sessionManager.createSessionWithPreset('/test/path2');
190
+ const eventEmitter1 = eventEmitters.get('/test/path1');
191
+ const eventEmitter2 = eventEmitters.get('/test/path2');
192
+ // Both should start as busy
193
+ expect(session1.state).toBe('busy');
194
+ expect(session2.state).toBe('busy');
195
+ // Simulate different outputs for each session
196
+ // Session 1 goes to idle
197
+ eventEmitter1.emit('data', 'Idle output for session 1');
198
+ // Session 2 goes to waiting_input
199
+ eventEmitter2.emit('data', '│ Do you want to continue?');
200
+ // Advance time to check but not confirm
201
+ vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS * 2);
202
+ // Both should have pending states but not changed yet
203
+ expect(session1.state).toBe('busy');
204
+ expect(session1.pendingState).toBe('idle');
205
+ expect(session2.state).toBe('busy');
206
+ expect(session2.pendingState).toBe('waiting_input');
207
+ // Advance time to confirm both
208
+ vi.advanceTimersByTime(STATE_PERSISTENCE_DURATION_MS);
209
+ // Both should now be in their new states
210
+ expect(session1.state).toBe('idle');
211
+ expect(session1.pendingState).toBeUndefined();
212
+ expect(session2.state).toBe('waiting_input');
213
+ expect(session2.pendingState).toBeUndefined();
214
+ });
215
+ });
@@ -1,19 +1,19 @@
1
1
  import { SessionState, Terminal, StateDetectionStrategy } from '../types/index.js';
2
2
  export interface StateDetector {
3
- detectState(terminal: Terminal): SessionState;
3
+ detectState(terminal: Terminal, currentState: SessionState): SessionState;
4
4
  }
5
5
  export declare function createStateDetector(strategy?: StateDetectionStrategy): StateDetector;
6
6
  export declare abstract class BaseStateDetector implements StateDetector {
7
- abstract detectState(terminal: Terminal): SessionState;
7
+ abstract detectState(terminal: Terminal, currentState: SessionState): SessionState;
8
8
  protected getTerminalLines(terminal: Terminal, maxLines?: number): string[];
9
9
  protected getTerminalContent(terminal: Terminal, maxLines?: number): string;
10
10
  }
11
11
  export declare class ClaudeStateDetector extends BaseStateDetector {
12
- detectState(terminal: Terminal): SessionState;
12
+ detectState(terminal: Terminal, currentState: SessionState): SessionState;
13
13
  }
14
14
  export declare class GeminiStateDetector extends BaseStateDetector {
15
- detectState(terminal: Terminal): SessionState;
15
+ detectState(terminal: Terminal, _currentState: SessionState): SessionState;
16
16
  }
17
17
  export declare class CodexStateDetector extends BaseStateDetector {
18
- detectState(terminal: Terminal): SessionState;
18
+ detectState(terminal: Terminal, _currentState: SessionState): SessionState;
19
19
  }
@@ -32,9 +32,13 @@ export class BaseStateDetector {
32
32
  }
33
33
  }
34
34
  export class ClaudeStateDetector extends BaseStateDetector {
35
- detectState(terminal) {
35
+ detectState(terminal, currentState) {
36
36
  const content = this.getTerminalContent(terminal);
37
37
  const lowerContent = content.toLowerCase();
38
+ // Check for ctrl+r toggle prompt - maintain current state
39
+ if (lowerContent.includes('ctrl+r to toggle')) {
40
+ return currentState;
41
+ }
38
42
  // Check for waiting prompts with box character
39
43
  if (content.includes('│ Do you want') ||
40
44
  content.includes('│ Would you like')) {
@@ -50,7 +54,7 @@ export class ClaudeStateDetector extends BaseStateDetector {
50
54
  }
51
55
  // https://github.com/google-gemini/gemini-cli/blob/main/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
52
56
  export class GeminiStateDetector extends BaseStateDetector {
53
- detectState(terminal) {
57
+ detectState(terminal, _currentState) {
54
58
  const content = this.getTerminalContent(terminal);
55
59
  const lowerContent = content.toLowerCase();
56
60
  // Check for waiting prompts with box character
@@ -68,7 +72,7 @@ export class GeminiStateDetector extends BaseStateDetector {
68
72
  }
69
73
  }
70
74
  export class CodexStateDetector extends BaseStateDetector {
71
- detectState(terminal) {
75
+ detectState(terminal, _currentState) {
72
76
  const content = this.getTerminalContent(terminal);
73
77
  const lowerContent = content.toLowerCase();
74
78
  // Check for waiting prompts
@@ -33,7 +33,7 @@ describe('ClaudeStateDetector', () => {
33
33
  '│ > ',
34
34
  ]);
35
35
  // Act
36
- const state = detector.detectState(terminal);
36
+ const state = detector.detectState(terminal, 'idle');
37
37
  // Assert
38
38
  expect(state).toBe('waiting_input');
39
39
  });
@@ -45,7 +45,7 @@ describe('ClaudeStateDetector', () => {
45
45
  '│ > ',
46
46
  ]);
47
47
  // Act
48
- const state = detector.detectState(terminal);
48
+ const state = detector.detectState(terminal, 'idle');
49
49
  // Assert
50
50
  expect(state).toBe('waiting_input');
51
51
  });
@@ -56,7 +56,7 @@ describe('ClaudeStateDetector', () => {
56
56
  'Press ESC to interrupt',
57
57
  ]);
58
58
  // Act
59
- const state = detector.detectState(terminal);
59
+ const state = detector.detectState(terminal, 'idle');
60
60
  // Assert
61
61
  expect(state).toBe('busy');
62
62
  });
@@ -67,7 +67,7 @@ describe('ClaudeStateDetector', () => {
67
67
  'press esc to interrupt the process',
68
68
  ]);
69
69
  // Act
70
- const state = detector.detectState(terminal);
70
+ const state = detector.detectState(terminal, 'idle');
71
71
  // Assert
72
72
  expect(state).toBe('busy');
73
73
  });
@@ -79,7 +79,7 @@ describe('ClaudeStateDetector', () => {
79
79
  '> ',
80
80
  ]);
81
81
  // Act
82
- const state = detector.detectState(terminal);
82
+ const state = detector.detectState(terminal, 'idle');
83
83
  // Assert
84
84
  expect(state).toBe('idle');
85
85
  });
@@ -87,7 +87,7 @@ describe('ClaudeStateDetector', () => {
87
87
  // Arrange
88
88
  terminal = createMockTerminal([]);
89
89
  // Act
90
- const state = detector.detectState(terminal);
90
+ const state = detector.detectState(terminal, 'idle');
91
91
  // Assert
92
92
  expect(state).toBe('idle');
93
93
  });
@@ -106,7 +106,7 @@ describe('ClaudeStateDetector', () => {
106
106
  }
107
107
  terminal = createMockTerminal(lines);
108
108
  // Act
109
- const state = detector.detectState(terminal);
109
+ const state = detector.detectState(terminal, 'idle');
110
110
  // Assert
111
111
  expect(state).toBe('idle'); // Should not detect the old prompt
112
112
  });
@@ -118,10 +118,42 @@ describe('ClaudeStateDetector', () => {
118
118
  '│ > ',
119
119
  ]);
120
120
  // Act
121
- const state = detector.detectState(terminal);
121
+ const state = detector.detectState(terminal, 'idle');
122
122
  // Assert
123
123
  expect(state).toBe('waiting_input'); // waiting_input should take precedence
124
124
  });
125
+ it('should maintain current state when "ctrl+r to toggle" is present', () => {
126
+ // Arrange
127
+ terminal = createMockTerminal([
128
+ 'Some output',
129
+ 'Press Ctrl+R to toggle history search',
130
+ 'More output',
131
+ ]);
132
+ // Act - test with different current states
133
+ const idleState = detector.detectState(terminal, 'idle');
134
+ const busyState = detector.detectState(terminal, 'busy');
135
+ const waitingState = detector.detectState(terminal, 'waiting_input');
136
+ // Assert - should maintain whatever the current state was
137
+ expect(idleState).toBe('idle');
138
+ expect(busyState).toBe('busy');
139
+ expect(waitingState).toBe('waiting_input');
140
+ });
141
+ it('should maintain current state for various "ctrl+r" patterns', () => {
142
+ // Arrange - test different case variations
143
+ const patterns = [
144
+ 'ctrl+r to toggle',
145
+ 'CTRL+R TO TOGGLE',
146
+ 'Ctrl+R to toggle history',
147
+ 'Press ctrl+r to toggle the search',
148
+ ];
149
+ for (const pattern of patterns) {
150
+ terminal = createMockTerminal(['Some output', pattern]);
151
+ // Act
152
+ const state = detector.detectState(terminal, 'busy');
153
+ // Assert - should maintain the current state
154
+ expect(state).toBe('busy');
155
+ }
156
+ });
125
157
  });
126
158
  });
127
159
  describe('GeminiStateDetector', () => {
@@ -157,7 +189,7 @@ describe('GeminiStateDetector', () => {
157
189
  '│ > ',
158
190
  ]);
159
191
  // Act
160
- const state = detector.detectState(terminal);
192
+ const state = detector.detectState(terminal, 'idle');
161
193
  // Assert
162
194
  expect(state).toBe('waiting_input');
163
195
  });
@@ -169,7 +201,7 @@ describe('GeminiStateDetector', () => {
169
201
  '│ > ',
170
202
  ]);
171
203
  // Act
172
- const state = detector.detectState(terminal);
204
+ const state = detector.detectState(terminal, 'idle');
173
205
  // Assert
174
206
  expect(state).toBe('waiting_input');
175
207
  });
@@ -181,7 +213,7 @@ describe('GeminiStateDetector', () => {
181
213
  '│ > ',
182
214
  ]);
183
215
  // Act
184
- const state = detector.detectState(terminal);
216
+ const state = detector.detectState(terminal, 'idle');
185
217
  // Assert
186
218
  expect(state).toBe('waiting_input');
187
219
  });
@@ -192,7 +224,7 @@ describe('GeminiStateDetector', () => {
192
224
  'Press ESC to cancel',
193
225
  ]);
194
226
  // Act
195
- const state = detector.detectState(terminal);
227
+ const state = detector.detectState(terminal, 'idle');
196
228
  // Assert
197
229
  expect(state).toBe('busy');
198
230
  });
@@ -203,7 +235,7 @@ describe('GeminiStateDetector', () => {
203
235
  'Press Esc to cancel the operation',
204
236
  ]);
205
237
  // Act
206
- const state = detector.detectState(terminal);
238
+ const state = detector.detectState(terminal, 'idle');
207
239
  // Assert
208
240
  expect(state).toBe('busy');
209
241
  });
@@ -214,7 +246,7 @@ describe('GeminiStateDetector', () => {
214
246
  'Type your message below',
215
247
  ]);
216
248
  // Act
217
- const state = detector.detectState(terminal);
249
+ const state = detector.detectState(terminal, 'idle');
218
250
  // Assert
219
251
  expect(state).toBe('idle');
220
252
  });
@@ -222,7 +254,7 @@ describe('GeminiStateDetector', () => {
222
254
  // Arrange
223
255
  terminal = createMockTerminal([]);
224
256
  // Act
225
- const state = detector.detectState(terminal);
257
+ const state = detector.detectState(terminal, 'idle');
226
258
  // Assert
227
259
  expect(state).toBe('idle');
228
260
  });
@@ -234,7 +266,7 @@ describe('GeminiStateDetector', () => {
234
266
  '│ > ',
235
267
  ]);
236
268
  // Act
237
- const state = detector.detectState(terminal);
269
+ const state = detector.detectState(terminal, 'idle');
238
270
  // Assert
239
271
  expect(state).toBe('waiting_input'); // waiting_input should take precedence
240
272
  });
@@ -267,7 +299,7 @@ describe('CodexStateDetector', () => {
267
299
  // Arrange
268
300
  terminal = createMockTerminal(['Some output', '│Allow execution?', '│ > ']);
269
301
  // Act
270
- const state = detector.detectState(terminal);
302
+ const state = detector.detectState(terminal, 'idle');
271
303
  // Assert
272
304
  expect(state).toBe('waiting_input');
273
305
  });
@@ -275,7 +307,7 @@ describe('CodexStateDetector', () => {
275
307
  // Arrange
276
308
  terminal = createMockTerminal(['Some output', 'Continue? [y/N]', '> ']);
277
309
  // Act
278
- const state = detector.detectState(terminal);
310
+ const state = detector.detectState(terminal, 'idle');
279
311
  // Assert
280
312
  expect(state).toBe('waiting_input');
281
313
  });
@@ -286,7 +318,7 @@ describe('CodexStateDetector', () => {
286
318
  'Press any key to continue...',
287
319
  ]);
288
320
  // Act
289
- const state = detector.detectState(terminal);
321
+ const state = detector.detectState(terminal, 'idle');
290
322
  // Assert
291
323
  expect(state).toBe('waiting_input');
292
324
  });
@@ -298,7 +330,7 @@ describe('CodexStateDetector', () => {
298
330
  'Working...',
299
331
  ]);
300
332
  // Act
301
- const state = detector.detectState(terminal);
333
+ const state = detector.detectState(terminal, 'idle');
302
334
  // Assert
303
335
  expect(state).toBe('busy');
304
336
  });
@@ -310,7 +342,7 @@ describe('CodexStateDetector', () => {
310
342
  'Working...',
311
343
  ]);
312
344
  // Act
313
- const state = detector.detectState(terminal);
345
+ const state = detector.detectState(terminal, 'idle');
314
346
  // Assert
315
347
  expect(state).toBe('busy');
316
348
  });
@@ -318,7 +350,7 @@ describe('CodexStateDetector', () => {
318
350
  // Arrange
319
351
  terminal = createMockTerminal(['Normal output', 'Some message', 'Ready']);
320
352
  // Act
321
- const state = detector.detectState(terminal);
353
+ const state = detector.detectState(terminal, 'idle');
322
354
  // Assert
323
355
  expect(state).toBe('idle');
324
356
  });
@@ -326,7 +358,7 @@ describe('CodexStateDetector', () => {
326
358
  // Arrange
327
359
  terminal = createMockTerminal(['press esc to cancel', '[y/N]']);
328
360
  // Act
329
- const state = detector.detectState(terminal);
361
+ const state = detector.detectState(terminal, 'idle');
330
362
  // Assert
331
363
  expect(state).toBe('waiting_input');
332
364
  });
@@ -22,11 +22,13 @@ export interface Session {
22
22
  lastActivity: Date;
23
23
  isActive: boolean;
24
24
  terminal: Terminal;
25
- stateCheckInterval?: NodeJS.Timeout;
26
- isPrimaryCommand?: boolean;
27
- commandConfig?: CommandConfig;
28
- detectionStrategy?: StateDetectionStrategy;
29
- devcontainerConfig?: DevcontainerConfig;
25
+ stateCheckInterval: NodeJS.Timeout | undefined;
26
+ isPrimaryCommand: boolean;
27
+ commandConfig: CommandConfig | undefined;
28
+ detectionStrategy: StateDetectionStrategy | undefined;
29
+ devcontainerConfig: DevcontainerConfig | undefined;
30
+ pendingState: SessionState | undefined;
31
+ pendingStateStart: number | undefined;
30
32
  }
31
33
  export interface SessionManager {
32
34
  sessions: Map<string, Session>;
@@ -257,6 +257,12 @@ describe('hookExecutor Integration Tests', () => {
257
257
  outputHistory: [],
258
258
  state: 'idle',
259
259
  stateCheckInterval: undefined,
260
+ isPrimaryCommand: true,
261
+ commandConfig: undefined,
262
+ detectionStrategy: 'claude',
263
+ devcontainerConfig: undefined,
264
+ pendingState: undefined,
265
+ pendingStateStart: undefined,
260
266
  lastActivity: new Date(),
261
267
  isActive: true,
262
268
  };
@@ -304,6 +310,12 @@ describe('hookExecutor Integration Tests', () => {
304
310
  outputHistory: [],
305
311
  state: 'idle',
306
312
  stateCheckInterval: undefined,
313
+ isPrimaryCommand: true,
314
+ commandConfig: undefined,
315
+ detectionStrategy: 'claude',
316
+ devcontainerConfig: undefined,
317
+ pendingState: undefined,
318
+ pendingStateStart: undefined,
307
319
  lastActivity: new Date(),
308
320
  isActive: true,
309
321
  };
@@ -349,6 +361,12 @@ describe('hookExecutor Integration Tests', () => {
349
361
  outputHistory: [],
350
362
  state: 'idle',
351
363
  stateCheckInterval: undefined,
364
+ isPrimaryCommand: true,
365
+ commandConfig: undefined,
366
+ detectionStrategy: 'claude',
367
+ devcontainerConfig: undefined,
368
+ pendingState: undefined,
369
+ pendingStateStart: undefined,
352
370
  lastActivity: new Date(),
353
371
  isActive: true,
354
372
  };
@@ -105,6 +105,13 @@ describe('prepareWorktreeItems', () => {
105
105
  lastActivity: new Date(),
106
106
  isActive: true,
107
107
  terminal: {},
108
+ stateCheckInterval: undefined,
109
+ isPrimaryCommand: true,
110
+ commandConfig: undefined,
111
+ detectionStrategy: 'claude',
112
+ devcontainerConfig: undefined,
113
+ pendingState: undefined,
114
+ pendingStateStart: undefined,
108
115
  };
109
116
  it('should prepare basic worktree without git status', () => {
110
117
  const items = prepareWorktreeItems([mockWorktree], []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",