ccmanager 3.2.7 → 3.2.9

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 (45) hide show
  1. package/README.md +0 -18
  2. package/dist/components/App.js +1 -1
  3. package/dist/components/ConfigureStatusHooks.js +1 -1
  4. package/dist/components/ConfigureWorktreeHooks.js +1 -1
  5. package/dist/components/DeleteWorktree.d.ts +1 -0
  6. package/dist/components/DeleteWorktree.js +3 -3
  7. package/dist/components/DeleteWorktree.test.js +51 -0
  8. package/dist/components/NewWorktree.js +2 -2
  9. package/dist/services/sessionManager.autoApproval.test.js +1 -1
  10. package/dist/services/sessionManager.js +1 -1
  11. package/dist/services/stateDetector/base.d.ts +7 -0
  12. package/dist/services/stateDetector/base.js +21 -0
  13. package/dist/services/stateDetector/claude.d.ts +5 -0
  14. package/dist/services/stateDetector/claude.js +26 -0
  15. package/dist/services/{__tests__/stateDetector.claude.test.js → stateDetector/claude.test.js} +44 -1
  16. package/dist/services/stateDetector/cline.d.ts +5 -0
  17. package/dist/services/stateDetector/cline.js +24 -0
  18. package/dist/services/{__tests__/stateDetector.cline.test.js → stateDetector/cline.test.js} +1 -1
  19. package/dist/services/stateDetector/codex.d.ts +5 -0
  20. package/dist/services/stateDetector/codex.js +27 -0
  21. package/dist/services/{__tests__/stateDetector.codex.test.js → stateDetector/codex.test.js} +1 -1
  22. package/dist/services/stateDetector/cursor.d.ts +5 -0
  23. package/dist/services/stateDetector/cursor.js +19 -0
  24. package/dist/services/{__tests__/stateDetector.cursor.test.js → stateDetector/cursor.test.js} +1 -1
  25. package/dist/services/stateDetector/gemini.d.ts +5 -0
  26. package/dist/services/stateDetector/gemini.js +28 -0
  27. package/dist/services/{__tests__/stateDetector.gemini.test.js → stateDetector/gemini.test.js} +1 -1
  28. package/dist/services/stateDetector/github-copilot.d.ts +5 -0
  29. package/dist/services/stateDetector/github-copilot.js +21 -0
  30. package/dist/services/{__tests__/stateDetector.github-copilot.test.js → stateDetector/github-copilot.test.js} +1 -1
  31. package/dist/services/stateDetector/index.d.ts +3 -0
  32. package/dist/services/stateDetector/index.js +24 -0
  33. package/dist/services/stateDetector/types.d.ts +4 -0
  34. package/dist/services/stateDetector/types.js +1 -0
  35. package/package.json +6 -6
  36. package/dist/services/stateDetector.d.ts +0 -28
  37. package/dist/services/stateDetector.js +0 -174
  38. /package/dist/services/{__tests__/stateDetector.claude.test.d.ts → stateDetector/claude.test.d.ts} +0 -0
  39. /package/dist/services/{__tests__/stateDetector.cline.test.d.ts → stateDetector/cline.test.d.ts} +0 -0
  40. /package/dist/services/{__tests__/stateDetector.codex.test.d.ts → stateDetector/codex.test.d.ts} +0 -0
  41. /package/dist/services/{__tests__/stateDetector.cursor.test.d.ts → stateDetector/cursor.test.d.ts} +0 -0
  42. /package/dist/services/{__tests__/stateDetector.gemini.test.d.ts → stateDetector/gemini.test.d.ts} +0 -0
  43. /package/dist/services/{__tests__/stateDetector.github-copilot.test.d.ts → stateDetector/github-copilot.test.d.ts} +0 -0
  44. /package/dist/services/{__tests__ → stateDetector}/testUtils.d.ts +0 -0
  45. /package/dist/services/{__tests__ → stateDetector}/testUtils.js +0 -0
package/README.md CHANGED
@@ -44,30 +44,12 @@ Claude Squad doesn't show session states in its menu, making it hard to know whi
44
44
  ### 🎯 Simple and intuitive interface
45
45
  Following Claude Code's philosophy, CCManager keeps things minimal and intuitive. The interface is so simple you'll understand it in seconds - no manual needed.
46
46
 
47
- ## Requirements
48
-
49
- - **Node.js 22 or later** is required to run CCManager.
50
-
51
47
  ## Install
52
48
 
53
49
  ```bash
54
50
  npm install -g ccmanager
55
51
  ```
56
52
 
57
- ### Using mise
58
-
59
- If you use [mise](https://mise.jdx.dev/) as a version manager, you can install CCManager with:
60
-
61
- ```bash
62
- mise install npm:ccmanager && mise use -g npm:ccmanager
63
- ```
64
-
65
- To run CCManager with Node.js 22:
66
-
67
- ```bash
68
- mise exec node@22 -- ccmanager
69
- ```
70
-
71
53
  ### Local Development
72
54
 
73
55
  ```bash
@@ -405,7 +405,7 @@ const App = ({ devcontainerConfig, multiProject }) => {
405
405
  React.createElement(Text, { color: "red" },
406
406
  "Error: ",
407
407
  error))),
408
- React.createElement(DeleteWorktree, { onComplete: handleDeleteWorktrees, onCancel: handleCancelDeleteWorktree })));
408
+ React.createElement(DeleteWorktree, { projectPath: selectedProject?.path, onComplete: handleDeleteWorktrees, onCancel: handleCancelDeleteWorktree })));
409
409
  }
410
410
  if (view === 'deleting-worktree') {
411
411
  // Compose message based on loading context
@@ -121,7 +121,7 @@ const ConfigureStatusHooks = ({ onComplete, }) => {
121
121
  React.createElement(Box, { marginTop: 1 },
122
122
  React.createElement(Text, { dimColor: true }, "Environment variables available: CCMANAGER_OLD_STATE, CCMANAGER_NEW_STATE,")),
123
123
  React.createElement(Box, null,
124
- React.createElement(Text, { dimColor: true }, "CCMANAGER_WORKTREE, CCMANAGER_WORKTREE_BRANCH, CCMANAGER_SESSION_ID")),
124
+ React.createElement(Text, { dimColor: true }, `CCMANAGER_WORKTREE_PATH, CCMANAGER_WORKTREE_BRANCH, CCMANAGER_SESSION_ID`)),
125
125
  React.createElement(Box, { marginTop: 1 },
126
126
  React.createElement(Text, { dimColor: true }, "Press Enter to save, Tab to toggle enabled, Esc to cancel"))));
127
127
  }
@@ -96,7 +96,7 @@ const ConfigureWorktreeHooks = ({ onComplete, }) => {
96
96
  currentEnabled ? '✓' : '✗',
97
97
  " (Press Tab to toggle)")),
98
98
  React.createElement(Box, { marginTop: 1 },
99
- React.createElement(Text, { dimColor: true }, "Environment variables available: CCMANAGER_WORKTREE, CCMANAGER_WORKTREE_BRANCH,")),
99
+ React.createElement(Text, { dimColor: true }, "Environment variables available: CCMANAGER_WORKTREE_PATH, CCMANAGER_WORKTREE_BRANCH,")),
100
100
  React.createElement(Box, null,
101
101
  React.createElement(Text, { dimColor: true }, "CCMANAGER_BASE_BRANCH, CCMANAGER_GIT_ROOT")),
102
102
  React.createElement(Box, { marginTop: 1 },
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  interface DeleteWorktreeProps {
3
+ projectPath?: string;
3
4
  onComplete: (worktreePaths: string[], deleteBranch: boolean) => void;
4
5
  onCancel: () => void;
5
6
  }
@@ -5,7 +5,7 @@ import { Effect } from 'effect';
5
5
  import { WorktreeService } from '../services/worktreeService.js';
6
6
  import DeleteConfirmation from './DeleteConfirmation.js';
7
7
  import { shortcutManager } from '../services/shortcutManager.js';
8
- const DeleteWorktree = ({ onComplete, onCancel, }) => {
8
+ const DeleteWorktree = ({ projectPath, onComplete, onCancel, }) => {
9
9
  const [worktrees, setWorktrees] = useState([]);
10
10
  const [selectedIndices, setSelectedIndices] = useState(new Set());
11
11
  const [confirmMode, setConfirmMode] = useState(false);
@@ -15,7 +15,7 @@ const DeleteWorktree = ({ onComplete, onCancel, }) => {
15
15
  useEffect(() => {
16
16
  let cancelled = false;
17
17
  const loadWorktrees = async () => {
18
- const worktreeService = new WorktreeService();
18
+ const worktreeService = new WorktreeService(projectPath);
19
19
  try {
20
20
  const allWorktrees = await Effect.runPromise(worktreeService.getWorktreesEffect());
21
21
  if (!cancelled) {
@@ -36,7 +36,7 @@ const DeleteWorktree = ({ onComplete, onCancel, }) => {
36
36
  return () => {
37
37
  cancelled = true;
38
38
  };
39
- }, []);
39
+ }, [projectPath]);
40
40
  // Create menu items from worktrees
41
41
  const menuItems = worktrees.map((worktree, index) => {
42
42
  const branchName = worktree.branch
@@ -40,6 +40,57 @@ describe('DeleteWorktree - Effect Integration', () => {
40
40
  beforeEach(() => {
41
41
  vi.clearAllMocks();
42
42
  });
43
+ it('should pass projectPath to WorktreeService when provided', async () => {
44
+ // GIVEN: projectPath is provided
45
+ const projectPath = '/test/project';
46
+ const mockWorktrees = [
47
+ {
48
+ path: '/test/project/wt1',
49
+ branch: 'feature-1',
50
+ isMainWorktree: false,
51
+ hasSession: false,
52
+ },
53
+ ];
54
+ const mockEffect = Effect.succeed(mockWorktrees);
55
+ vi.mocked(WorktreeService).mockImplementation(function () {
56
+ return {
57
+ getWorktreesEffect: vi.fn(() => mockEffect),
58
+ };
59
+ });
60
+ const onComplete = vi.fn();
61
+ const onCancel = vi.fn();
62
+ // WHEN: Component renders with projectPath
63
+ render(React.createElement(DeleteWorktree, { projectPath: projectPath, onComplete: onComplete, onCancel: onCancel }));
64
+ // Wait for Effect to execute
65
+ await new Promise(resolve => setTimeout(resolve, 50));
66
+ // THEN: WorktreeService was called with projectPath
67
+ expect(WorktreeService).toHaveBeenCalledWith(projectPath);
68
+ });
69
+ it('should use undefined when projectPath not provided', async () => {
70
+ // GIVEN: No projectPath
71
+ const mockWorktrees = [
72
+ {
73
+ path: '/test/wt1',
74
+ branch: 'feature-1',
75
+ isMainWorktree: false,
76
+ hasSession: false,
77
+ },
78
+ ];
79
+ const mockEffect = Effect.succeed(mockWorktrees);
80
+ vi.mocked(WorktreeService).mockImplementation(function () {
81
+ return {
82
+ getWorktreesEffect: vi.fn(() => mockEffect),
83
+ };
84
+ });
85
+ const onComplete = vi.fn();
86
+ const onCancel = vi.fn();
87
+ // WHEN: Component renders without projectPath
88
+ render(React.createElement(DeleteWorktree, { onComplete: onComplete, onCancel: onCancel }));
89
+ // Wait for Effect to execute
90
+ await new Promise(resolve => setTimeout(resolve, 50));
91
+ // THEN: WorktreeService was called with undefined (defaults to cwd)
92
+ expect(WorktreeService).toHaveBeenCalledWith(undefined);
93
+ });
43
94
  it('should load worktrees using Effect-based method', async () => {
44
95
  // GIVEN: Mock worktrees returned by Effect
45
96
  const mockWorktrees = [
@@ -27,7 +27,7 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
27
27
  // Initialize worktree service and load branches using Effect
28
28
  useEffect(() => {
29
29
  let cancelled = false;
30
- const service = new WorktreeService();
30
+ const service = new WorktreeService(projectPath);
31
31
  const loadBranches = async () => {
32
32
  // Use Effect.all to load branches and defaultBranch in parallel
33
33
  const workflow = Effect.all([service.getAllBranchesEffect(), service.getDefaultBranchEffect()], { concurrency: 2 });
@@ -63,7 +63,7 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
63
63
  return () => {
64
64
  cancelled = true;
65
65
  };
66
- }, []);
66
+ }, [projectPath]);
67
67
  // Create branch items with default branch first (memoized)
68
68
  const allBranchItems = useMemo(() => [
69
69
  { label: `${defaultBranch} (default)`, value: defaultBranch },
@@ -14,7 +14,7 @@ vi.mock('./bunTerminal.js', () => ({
14
14
  return null;
15
15
  }),
16
16
  }));
17
- vi.mock('./stateDetector.js', () => ({
17
+ vi.mock('./stateDetector/index.js', () => ({
18
18
  createStateDetector: () => ({ detectState: detectStateMock }),
19
19
  }));
20
20
  vi.mock('./configurationManager.js', () => ({
@@ -5,7 +5,7 @@ import { exec } from 'child_process';
5
5
  import { promisify } from 'util';
6
6
  import { configurationManager } from './configurationManager.js';
7
7
  import { executeStatusHook } from '../utils/hookExecutor.js';
8
- import { createStateDetector } from './stateDetector.js';
8
+ import { createStateDetector } from './stateDetector/index.js';
9
9
  import { STATE_PERSISTENCE_DURATION_MS, STATE_CHECK_INTERVAL_MS, } from '../constants/statePersistence.js';
10
10
  import { Effect } from 'effect';
11
11
  import { ProcessError, ConfigError } from '../types/errors.js';
@@ -0,0 +1,7 @@
1
+ import { SessionState, Terminal } from '../../types/index.js';
2
+ import { StateDetector } from './types.js';
3
+ export declare abstract class BaseStateDetector implements StateDetector {
4
+ abstract detectState(terminal: Terminal, currentState: SessionState): SessionState;
5
+ protected getTerminalLines(terminal: Terminal, maxLines?: number): string[];
6
+ protected getTerminalContent(terminal: Terminal, maxLines?: number): string;
7
+ }
@@ -0,0 +1,21 @@
1
+ export class BaseStateDetector {
2
+ getTerminalLines(terminal, maxLines = 30) {
3
+ const buffer = terminal.buffer.active;
4
+ const lines = [];
5
+ // Start from the bottom and work our way up
6
+ for (let i = buffer.length - 1; i >= 0 && lines.length < maxLines; i--) {
7
+ const line = buffer.getLine(i);
8
+ if (line) {
9
+ const text = line.translateToString(true);
10
+ // Skip empty lines at the bottom
11
+ if (lines.length > 0 || text.trim() !== '') {
12
+ lines.unshift(text);
13
+ }
14
+ }
15
+ }
16
+ return lines;
17
+ }
18
+ getTerminalContent(terminal, maxLines = 30) {
19
+ return this.getTerminalLines(terminal, maxLines).join('\n');
20
+ }
21
+ }
@@ -0,0 +1,5 @@
1
+ import { SessionState, Terminal } from '../../types/index.js';
2
+ import { BaseStateDetector } from './base.js';
3
+ export declare class ClaudeStateDetector extends BaseStateDetector {
4
+ detectState(terminal: Terminal, currentState: SessionState): SessionState;
5
+ }
@@ -0,0 +1,26 @@
1
+ import { BaseStateDetector } from './base.js';
2
+ export class ClaudeStateDetector extends BaseStateDetector {
3
+ detectState(terminal, currentState) {
4
+ const content = this.getTerminalContent(terminal);
5
+ const lowerContent = content.toLowerCase();
6
+ // Check for ctrl+r toggle prompt - maintain current state
7
+ if (lowerContent.includes('ctrl+r to toggle')) {
8
+ return currentState;
9
+ }
10
+ // Check for "Do you want" or "Would you like" pattern with options
11
+ // Handles both simple ("Do you want...\nYes") and complex (numbered options) formats
12
+ if (/(?:do you want|would you like).+\n+[\s\S]*?(?:yes|❯)/.test(lowerContent)) {
13
+ return 'waiting_input';
14
+ }
15
+ // Check for "esc to cancel" - indicates waiting for user input
16
+ if (lowerContent.includes('esc to cancel')) {
17
+ return 'waiting_input';
18
+ }
19
+ // Check for busy state
20
+ if (lowerContent.includes('esc to interrupt')) {
21
+ return 'busy';
22
+ }
23
+ // Otherwise idle
24
+ return 'idle';
25
+ }
26
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { ClaudeStateDetector } from '../stateDetector.js';
2
+ import { ClaudeStateDetector } from './claude.js';
3
3
  import { createMockTerminal } from './testUtils.js';
4
4
  describe('ClaudeStateDetector', () => {
5
5
  let detector;
@@ -167,5 +167,48 @@ describe('ClaudeStateDetector', () => {
167
167
  // Assert
168
168
  expect(state).toBe('waiting_input');
169
169
  });
170
+ it('should detect waiting_input when "Yes" has characters before it (e.g., "❯ 1. Yes")', () => {
171
+ // Arrange
172
+ terminal = createMockTerminal([
173
+ 'Do you want to continue?',
174
+ '❯ 1. Yes',
175
+ ' 2. No',
176
+ ]);
177
+ // Act
178
+ const state = detector.detectState(terminal, 'idle');
179
+ // Assert
180
+ expect(state).toBe('waiting_input');
181
+ });
182
+ it('should detect waiting_input when "esc to cancel" is present', () => {
183
+ // Arrange
184
+ terminal = createMockTerminal([
185
+ 'Enter your message:',
186
+ 'Press esc to cancel',
187
+ ]);
188
+ // Act
189
+ const state = detector.detectState(terminal, 'idle');
190
+ // Assert
191
+ expect(state).toBe('waiting_input');
192
+ });
193
+ it('should detect waiting_input when "esc to cancel" is present (case insensitive)', () => {
194
+ // Arrange
195
+ terminal = createMockTerminal(['Waiting for input', 'ESC TO CANCEL']);
196
+ // Act
197
+ const state = detector.detectState(terminal, 'idle');
198
+ // Assert
199
+ expect(state).toBe('waiting_input');
200
+ });
201
+ it('should prioritize "esc to cancel" over "esc to interrupt" when both present', () => {
202
+ // Arrange
203
+ terminal = createMockTerminal([
204
+ 'Press esc to interrupt',
205
+ 'Some input prompt',
206
+ 'Press esc to cancel',
207
+ ]);
208
+ // Act
209
+ const state = detector.detectState(terminal, 'idle');
210
+ // Assert
211
+ expect(state).toBe('waiting_input');
212
+ });
170
213
  });
171
214
  });
@@ -0,0 +1,5 @@
1
+ import { SessionState, Terminal } from '../../types/index.js';
2
+ import { BaseStateDetector } from './base.js';
3
+ export declare class ClineStateDetector extends BaseStateDetector {
4
+ detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ }
@@ -0,0 +1,24 @@
1
+ import { BaseStateDetector } from './base.js';
2
+ // https://github.com/cline/cline/blob/580db36476b6b52def03c8aeda325aae1c817cde/cli/pkg/cli/task/input_handler.go
3
+ export class ClineStateDetector extends BaseStateDetector {
4
+ detectState(terminal, _currentState) {
5
+ const content = this.getTerminalContent(terminal);
6
+ const lowerContent = content.toLowerCase();
7
+ // Check for waiting prompts with tool permission - Priority 1
8
+ // Pattern: [\[act|plan\] mode].*?\n.*yes (when mode indicator present)
9
+ // Or simply: let cline use this tool (distinctive text)
10
+ if (/\[(act|plan) mode\].*?\n.*yes/i.test(lowerContent) ||
11
+ /let cline use this tool/i.test(lowerContent)) {
12
+ return 'waiting_input';
13
+ }
14
+ // Check for idle state - Priority 2
15
+ // Pattern: [\[act|plan\] mode].*Cline is ready for your message... (when mode indicator present)
16
+ // Or simply: cline is ready for your message (distinctive text)
17
+ if (/\[(act|plan) mode\].*cline is ready for your message/i.test(lowerContent) ||
18
+ /cline is ready for your message/i.test(lowerContent)) {
19
+ return 'idle';
20
+ }
21
+ // Otherwise busy - Priority 3
22
+ return 'busy';
23
+ }
24
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { ClineStateDetector } from '../stateDetector.js';
2
+ import { ClineStateDetector } from './cline.js';
3
3
  import { createMockTerminal } from './testUtils.js';
4
4
  describe('ClineStateDetector', () => {
5
5
  let detector;
@@ -0,0 +1,5 @@
1
+ import { SessionState, Terminal } from '../../types/index.js';
2
+ import { BaseStateDetector } from './base.js';
3
+ export declare class CodexStateDetector extends BaseStateDetector {
4
+ detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ }
@@ -0,0 +1,27 @@
1
+ import { BaseStateDetector } from './base.js';
2
+ export class CodexStateDetector extends BaseStateDetector {
3
+ detectState(terminal, _currentState) {
4
+ const content = this.getTerminalContent(terminal);
5
+ const lowerContent = content.toLowerCase();
6
+ // Check for confirmation prompt patterns - highest priority
7
+ if (lowerContent.includes('press enter to confirm or esc to cancel') ||
8
+ /confirm with .+ enter/i.test(content)) {
9
+ return 'waiting_input';
10
+ }
11
+ // Check for waiting prompts
12
+ if (lowerContent.includes('allow command?') ||
13
+ lowerContent.includes('[y/n]') ||
14
+ lowerContent.includes('yes (y)')) {
15
+ return 'waiting_input';
16
+ }
17
+ if (/(do you want|would you like)[\s\S]*?\n+[\s\S]*?\byes\b/.test(lowerContent)) {
18
+ return 'waiting_input';
19
+ }
20
+ // Check for busy state
21
+ if (/esc.*interrupt/i.test(lowerContent)) {
22
+ return 'busy';
23
+ }
24
+ // Otherwise idle
25
+ return 'idle';
26
+ }
27
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { CodexStateDetector } from '../stateDetector.js';
2
+ import { CodexStateDetector } from './codex.js';
3
3
  import { createMockTerminal } from './testUtils.js';
4
4
  describe('CodexStateDetector', () => {
5
5
  let detector;
@@ -0,0 +1,5 @@
1
+ import { SessionState, Terminal } from '../../types/index.js';
2
+ import { BaseStateDetector } from './base.js';
3
+ export declare class CursorStateDetector extends BaseStateDetector {
4
+ detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ }
@@ -0,0 +1,19 @@
1
+ import { BaseStateDetector } from './base.js';
2
+ export class CursorStateDetector extends BaseStateDetector {
3
+ detectState(terminal, _currentState) {
4
+ const content = this.getTerminalContent(terminal);
5
+ const lowerContent = content.toLowerCase();
6
+ // Check for waiting prompts - Priority 1
7
+ if (lowerContent.includes('(y) (enter)') ||
8
+ lowerContent.includes('keep (n)') ||
9
+ /auto .* \(shift\+tab\)/.test(lowerContent)) {
10
+ return 'waiting_input';
11
+ }
12
+ // Check for busy state - Priority 2
13
+ if (lowerContent.includes('ctrl+c to stop')) {
14
+ return 'busy';
15
+ }
16
+ // Otherwise idle - Priority 3
17
+ return 'idle';
18
+ }
19
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { CursorStateDetector } from '../stateDetector.js';
2
+ import { CursorStateDetector } from './cursor.js';
3
3
  import { createMockTerminal } from './testUtils.js';
4
4
  describe('CursorStateDetector', () => {
5
5
  let detector;
@@ -0,0 +1,5 @@
1
+ import { SessionState, Terminal } from '../../types/index.js';
2
+ import { BaseStateDetector } from './base.js';
3
+ export declare class GeminiStateDetector extends BaseStateDetector {
4
+ detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ }
@@ -0,0 +1,28 @@
1
+ import { BaseStateDetector } from './base.js';
2
+ // https://github.com/google-gemini/gemini-cli/blob/main/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
3
+ export class GeminiStateDetector extends BaseStateDetector {
4
+ detectState(terminal, _currentState) {
5
+ const content = this.getTerminalContent(terminal);
6
+ const lowerContent = content.toLowerCase();
7
+ // Check for explicit user confirmation message - highest priority
8
+ if (lowerContent.includes('waiting for user confirmation')) {
9
+ return 'waiting_input';
10
+ }
11
+ // Check for waiting prompts with box character
12
+ if (content.includes('│ Apply this change') ||
13
+ content.includes('│ Allow execution') ||
14
+ content.includes('│ Do you want to proceed')) {
15
+ return 'waiting_input';
16
+ }
17
+ // Check for multiline confirmation prompts ending with "yes"
18
+ if (/(allow execution|do you want to|apply this change)[\s\S]*?\n+[\s\S]*?\byes\b/.test(lowerContent)) {
19
+ return 'waiting_input';
20
+ }
21
+ // Check for busy state
22
+ if (lowerContent.includes('esc to cancel')) {
23
+ return 'busy';
24
+ }
25
+ // Otherwise idle
26
+ return 'idle';
27
+ }
28
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { GeminiStateDetector } from '../stateDetector.js';
2
+ import { GeminiStateDetector } from './gemini.js';
3
3
  import { createMockTerminal } from './testUtils.js';
4
4
  describe('GeminiStateDetector', () => {
5
5
  let detector;
@@ -0,0 +1,5 @@
1
+ import { SessionState, Terminal } from '../../types/index.js';
2
+ import { BaseStateDetector } from './base.js';
3
+ export declare class GitHubCopilotStateDetector extends BaseStateDetector {
4
+ detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ }
@@ -0,0 +1,21 @@
1
+ import { BaseStateDetector } from './base.js';
2
+ export class GitHubCopilotStateDetector extends BaseStateDetector {
3
+ detectState(terminal, _currentState) {
4
+ const content = this.getTerminalContent(terminal);
5
+ const lowerContent = content.toLowerCase();
6
+ // Check for confirmation prompt pattern - highest priority
7
+ if (/confirm with .+ enter/i.test(content)) {
8
+ return 'waiting_input';
9
+ }
10
+ // Waiting prompt has priority 2
11
+ if (lowerContent.includes('│ do you want')) {
12
+ return 'waiting_input';
13
+ }
14
+ // Busy state detection has priority 3
15
+ if (lowerContent.includes('esc to cancel')) {
16
+ return 'busy';
17
+ }
18
+ // Otherwise idle as priority 4
19
+ return 'idle';
20
+ }
21
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { GitHubCopilotStateDetector } from '../stateDetector.js';
2
+ import { GitHubCopilotStateDetector } from './github-copilot.js';
3
3
  import { createMockTerminal } from './testUtils.js';
4
4
  describe('GitHubCopilotStateDetector', () => {
5
5
  let detector;
@@ -0,0 +1,3 @@
1
+ import { StateDetectionStrategy } from '../../types/index.js';
2
+ import { StateDetector } from './types.js';
3
+ export declare function createStateDetector(strategy?: StateDetectionStrategy): StateDetector;
@@ -0,0 +1,24 @@
1
+ import { ClaudeStateDetector } from './claude.js';
2
+ import { GeminiStateDetector } from './gemini.js';
3
+ import { CodexStateDetector } from './codex.js';
4
+ import { CursorStateDetector } from './cursor.js';
5
+ import { GitHubCopilotStateDetector } from './github-copilot.js';
6
+ import { ClineStateDetector } from './cline.js';
7
+ export function createStateDetector(strategy = 'claude') {
8
+ switch (strategy) {
9
+ case 'claude':
10
+ return new ClaudeStateDetector();
11
+ case 'gemini':
12
+ return new GeminiStateDetector();
13
+ case 'codex':
14
+ return new CodexStateDetector();
15
+ case 'cursor':
16
+ return new CursorStateDetector();
17
+ case 'github-copilot':
18
+ return new GitHubCopilotStateDetector();
19
+ case 'cline':
20
+ return new ClineStateDetector();
21
+ default:
22
+ return new ClaudeStateDetector();
23
+ }
24
+ }
@@ -0,0 +1,4 @@
1
+ import { SessionState, Terminal } from '../../types/index.js';
2
+ export interface StateDetector {
3
+ detectState(terminal: Terminal, currentState: SessionState): SessionState;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "3.2.7",
3
+ "version": "3.2.9",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",
@@ -41,11 +41,11 @@
41
41
  "bin"
42
42
  ],
43
43
  "optionalDependencies": {
44
- "@kodaikabasawa/ccmanager-darwin-arm64": "3.2.7",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "3.2.7",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "3.2.7",
47
- "@kodaikabasawa/ccmanager-linux-x64": "3.2.7",
48
- "@kodaikabasawa/ccmanager-win32-x64": "3.2.7"
44
+ "@kodaikabasawa/ccmanager-darwin-arm64": "3.2.9",
45
+ "@kodaikabasawa/ccmanager-darwin-x64": "3.2.9",
46
+ "@kodaikabasawa/ccmanager-linux-arm64": "3.2.9",
47
+ "@kodaikabasawa/ccmanager-linux-x64": "3.2.9",
48
+ "@kodaikabasawa/ccmanager-win32-x64": "3.2.9"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.28.0",
@@ -1,28 +0,0 @@
1
- import { SessionState, Terminal, StateDetectionStrategy } from '../types/index.js';
2
- export interface StateDetector {
3
- detectState(terminal: Terminal, currentState: SessionState): SessionState;
4
- }
5
- export declare function createStateDetector(strategy?: StateDetectionStrategy): StateDetector;
6
- export declare abstract class BaseStateDetector implements StateDetector {
7
- abstract detectState(terminal: Terminal, currentState: SessionState): SessionState;
8
- protected getTerminalLines(terminal: Terminal, maxLines?: number): string[];
9
- protected getTerminalContent(terminal: Terminal, maxLines?: number): string;
10
- }
11
- export declare class ClaudeStateDetector extends BaseStateDetector {
12
- detectState(terminal: Terminal, currentState: SessionState): SessionState;
13
- }
14
- export declare class GeminiStateDetector extends BaseStateDetector {
15
- detectState(terminal: Terminal, _currentState: SessionState): SessionState;
16
- }
17
- export declare class CodexStateDetector extends BaseStateDetector {
18
- detectState(terminal: Terminal, _currentState: SessionState): SessionState;
19
- }
20
- export declare class CursorStateDetector extends BaseStateDetector {
21
- detectState(terminal: Terminal, _currentState: SessionState): SessionState;
22
- }
23
- export declare class GitHubCopilotStateDetector extends BaseStateDetector {
24
- detectState(terminal: Terminal, _currentState: SessionState): SessionState;
25
- }
26
- export declare class ClineStateDetector extends BaseStateDetector {
27
- detectState(terminal: Terminal, _currentState: SessionState): SessionState;
28
- }
@@ -1,174 +0,0 @@
1
- export function createStateDetector(strategy = 'claude') {
2
- switch (strategy) {
3
- case 'claude':
4
- return new ClaudeStateDetector();
5
- case 'gemini':
6
- return new GeminiStateDetector();
7
- case 'codex':
8
- return new CodexStateDetector();
9
- case 'cursor':
10
- return new CursorStateDetector();
11
- case 'github-copilot':
12
- return new GitHubCopilotStateDetector();
13
- case 'cline':
14
- return new ClineStateDetector();
15
- default:
16
- return new ClaudeStateDetector();
17
- }
18
- }
19
- export class BaseStateDetector {
20
- getTerminalLines(terminal, maxLines = 30) {
21
- const buffer = terminal.buffer.active;
22
- const lines = [];
23
- // Start from the bottom and work our way up
24
- for (let i = buffer.length - 1; i >= 0 && lines.length < maxLines; i--) {
25
- const line = buffer.getLine(i);
26
- if (line) {
27
- const text = line.translateToString(true);
28
- // Skip empty lines at the bottom
29
- if (lines.length > 0 || text.trim() !== '') {
30
- lines.unshift(text);
31
- }
32
- }
33
- }
34
- return lines;
35
- }
36
- getTerminalContent(terminal, maxLines = 30) {
37
- return this.getTerminalLines(terminal, maxLines).join('\n');
38
- }
39
- }
40
- export class ClaudeStateDetector extends BaseStateDetector {
41
- detectState(terminal, currentState) {
42
- const content = this.getTerminalContent(terminal);
43
- const lowerContent = content.toLowerCase();
44
- // Check for ctrl+r toggle prompt - maintain current state
45
- if (lowerContent.includes('ctrl+r to toggle')) {
46
- return currentState;
47
- }
48
- // Check for "Do you want" or "Would you like" pattern with options
49
- // Handles both simple ("Do you want...\nYes") and complex (numbered options) formats
50
- if (/(?:do you want|would you like).+\n+[\s\S]*?(?:yes|❯)/.test(lowerContent)) {
51
- return 'waiting_input';
52
- }
53
- // Check for busy state
54
- if (lowerContent.includes('esc to interrupt')) {
55
- return 'busy';
56
- }
57
- // Otherwise idle
58
- return 'idle';
59
- }
60
- }
61
- // https://github.com/google-gemini/gemini-cli/blob/main/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
62
- export class GeminiStateDetector extends BaseStateDetector {
63
- detectState(terminal, _currentState) {
64
- const content = this.getTerminalContent(terminal);
65
- const lowerContent = content.toLowerCase();
66
- // Check for explicit user confirmation message - highest priority
67
- if (lowerContent.includes('waiting for user confirmation')) {
68
- return 'waiting_input';
69
- }
70
- // Check for waiting prompts with box character
71
- if (content.includes('│ Apply this change') ||
72
- content.includes('│ Allow execution') ||
73
- content.includes('│ Do you want to proceed')) {
74
- return 'waiting_input';
75
- }
76
- // Check for multiline confirmation prompts ending with "yes"
77
- if (/(allow execution|do you want to|apply this change)[\s\S]*?\n+[\s\S]*?\byes\b/.test(lowerContent)) {
78
- return 'waiting_input';
79
- }
80
- // Check for busy state
81
- if (lowerContent.includes('esc to cancel')) {
82
- return 'busy';
83
- }
84
- // Otherwise idle
85
- return 'idle';
86
- }
87
- }
88
- export class CodexStateDetector extends BaseStateDetector {
89
- detectState(terminal, _currentState) {
90
- const content = this.getTerminalContent(terminal);
91
- const lowerContent = content.toLowerCase();
92
- // Check for confirmation prompt patterns - highest priority
93
- if (lowerContent.includes('press enter to confirm or esc to cancel') ||
94
- /confirm with .+ enter/i.test(content)) {
95
- return 'waiting_input';
96
- }
97
- // Check for waiting prompts
98
- if (lowerContent.includes('allow command?') ||
99
- lowerContent.includes('[y/n]') ||
100
- lowerContent.includes('yes (y)')) {
101
- return 'waiting_input';
102
- }
103
- if (/(do you want|would you like)[\s\S]*?\n+[\s\S]*?\byes\b/.test(lowerContent)) {
104
- return 'waiting_input';
105
- }
106
- // Check for busy state
107
- if (/esc.*interrupt/i.test(lowerContent)) {
108
- return 'busy';
109
- }
110
- // Otherwise idle
111
- return 'idle';
112
- }
113
- }
114
- export class CursorStateDetector extends BaseStateDetector {
115
- detectState(terminal, _currentState) {
116
- const content = this.getTerminalContent(terminal);
117
- const lowerContent = content.toLowerCase();
118
- // Check for waiting prompts - Priority 1
119
- if (lowerContent.includes('(y) (enter)') ||
120
- lowerContent.includes('keep (n)') ||
121
- /auto .* \(shift\+tab\)/.test(lowerContent)) {
122
- return 'waiting_input';
123
- }
124
- // Check for busy state - Priority 2
125
- if (lowerContent.includes('ctrl+c to stop')) {
126
- return 'busy';
127
- }
128
- // Otherwise idle - Priority 3
129
- return 'idle';
130
- }
131
- }
132
- export class GitHubCopilotStateDetector extends BaseStateDetector {
133
- detectState(terminal, _currentState) {
134
- const content = this.getTerminalContent(terminal);
135
- const lowerContent = content.toLowerCase();
136
- // Check for confirmation prompt pattern - highest priority
137
- if (/confirm with .+ enter/i.test(content)) {
138
- return 'waiting_input';
139
- }
140
- // Waiting prompt has priority 2
141
- if (lowerContent.includes('│ do you want')) {
142
- return 'waiting_input';
143
- }
144
- // Busy state detection has priority 3
145
- if (lowerContent.includes('esc to cancel')) {
146
- return 'busy';
147
- }
148
- // Otherwise idle as priority 4
149
- return 'idle';
150
- }
151
- }
152
- // https://github.com/cline/cline/blob/580db36476b6b52def03c8aeda325aae1c817cde/cli/pkg/cli/task/input_handler.go
153
- export class ClineStateDetector extends BaseStateDetector {
154
- detectState(terminal, _currentState) {
155
- const content = this.getTerminalContent(terminal);
156
- const lowerContent = content.toLowerCase();
157
- // Check for waiting prompts with tool permission - Priority 1
158
- // Pattern: [\[act|plan\] mode].*?\n.*yes (when mode indicator present)
159
- // Or simply: let cline use this tool (distinctive text)
160
- if (/\[(act|plan) mode\].*?\n.*yes/i.test(lowerContent) ||
161
- /let cline use this tool/i.test(lowerContent)) {
162
- return 'waiting_input';
163
- }
164
- // Check for idle state - Priority 2
165
- // Pattern: [\[act|plan\] mode].*Cline is ready for your message... (when mode indicator present)
166
- // Or simply: cline is ready for your message (distinctive text)
167
- if (/\[(act|plan) mode\].*cline is ready for your message/i.test(lowerContent) ||
168
- /cline is ready for your message/i.test(lowerContent)) {
169
- return 'idle';
170
- }
171
- // Otherwise busy - Priority 3
172
- return 'busy';
173
- }
174
- }