ccmanager 2.5.1 → 2.6.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.
@@ -5,6 +5,20 @@ import SelectInput from 'ink-select-input';
5
5
  import { configurationManager } from '../services/configurationManager.js';
6
6
  import { shortcutManager } from '../services/shortcutManager.js';
7
7
  import Confirmation from './Confirmation.js';
8
+ // This function ensures all strategies are included at compile time
9
+ const createStrategyItems = () => {
10
+ // This object MUST include all StateDetectionStrategy values as keys
11
+ // If any are missing, TypeScript will error
12
+ const strategies = {
13
+ claude: { label: 'Claude', value: 'claude' },
14
+ gemini: { label: 'Gemini', value: 'gemini' },
15
+ codex: { label: 'Codex', value: 'codex' },
16
+ cursor: { label: 'Cursor Agent', value: 'cursor' },
17
+ };
18
+ return Object.values(strategies);
19
+ };
20
+ // Type-safe strategy items that ensures all StateDetectionStrategy values are included
21
+ const ALL_STRATEGY_ITEMS = createStrategyItems();
8
22
  const formatDetectionStrategy = (strategy) => {
9
23
  const value = strategy || 'claude';
10
24
  switch (value) {
@@ -12,6 +26,8 @@ const formatDetectionStrategy = (strategy) => {
12
26
  return 'Gemini';
13
27
  case 'codex':
14
28
  return 'Codex';
29
+ case 'cursor':
30
+ return 'Cursor';
15
31
  default:
16
32
  return 'Claude';
17
33
  }
@@ -259,11 +275,7 @@ const ConfigureCommand = ({ onComplete }) => {
259
275
  const preset = presets.find(p => p.id === selectedPresetId);
260
276
  if (!preset)
261
277
  return null;
262
- const strategyItems = [
263
- { label: 'Claude', value: 'claude' },
264
- { label: 'Gemini', value: 'gemini' },
265
- { label: 'Codex', value: 'codex' },
266
- ];
278
+ const strategyItems = ALL_STRATEGY_ITEMS;
267
279
  const currentStrategy = preset.detectionStrategy || 'claude';
268
280
  const initialIndex = strategyItems.findIndex(item => item.value === currentStrategy);
269
281
  return (React.createElement(Box, { flexDirection: "column" },
@@ -308,11 +320,7 @@ const ConfigureCommand = ({ onComplete }) => {
308
320
  // Render add preset form
309
321
  if (viewMode === 'add') {
310
322
  if (isSelectingStrategyInAdd) {
311
- const strategyItems = [
312
- { label: 'Claude', value: 'claude' },
313
- { label: 'Gemini', value: 'gemini' },
314
- { label: 'Codex', value: 'codex' },
315
- ];
323
+ const strategyItems = ALL_STRATEGY_ITEMS;
316
324
  return (React.createElement(Box, { flexDirection: "column" },
317
325
  React.createElement(Box, { marginBottom: 1 },
318
326
  React.createElement(Text, { bold: true, color: "green" }, "Add New Preset - Detection Strategy")),
@@ -17,3 +17,6 @@ export declare class GeminiStateDetector extends BaseStateDetector {
17
17
  export declare class CodexStateDetector extends BaseStateDetector {
18
18
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
19
19
  }
20
+ export declare class CursorStateDetector extends BaseStateDetector {
21
+ detectState(terminal: Terminal, _currentState: SessionState): SessionState;
22
+ }
@@ -6,6 +6,8 @@ export function createStateDetector(strategy = 'claude') {
6
6
  return new GeminiStateDetector();
7
7
  case 'codex':
8
8
  return new CodexStateDetector();
9
+ case 'cursor':
10
+ return new CursorStateDetector();
9
11
  default:
10
12
  return new ClaudeStateDetector();
11
13
  }
@@ -89,3 +91,21 @@ export class CodexStateDetector extends BaseStateDetector {
89
91
  return 'idle';
90
92
  }
91
93
  }
94
+ export class CursorStateDetector extends BaseStateDetector {
95
+ detectState(terminal, _currentState) {
96
+ const content = this.getTerminalContent(terminal);
97
+ const lowerContent = content.toLowerCase();
98
+ // Check for waiting prompts - Priority 1
99
+ if (lowerContent.includes('(y) (enter)') ||
100
+ lowerContent.includes('keep (n)') ||
101
+ /auto .* \(shift\+tab\)/.test(lowerContent)) {
102
+ return 'waiting_input';
103
+ }
104
+ // Check for busy state - Priority 2
105
+ if (lowerContent.includes('ctrl+c to stop')) {
106
+ return 'busy';
107
+ }
108
+ // Otherwise idle - Priority 3
109
+ return 'idle';
110
+ }
111
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { ClaudeStateDetector, GeminiStateDetector, CodexStateDetector, } from './stateDetector.js';
2
+ import { ClaudeStateDetector, GeminiStateDetector, CodexStateDetector, CursorStateDetector, } from './stateDetector.js';
3
3
  describe('ClaudeStateDetector', () => {
4
4
  let detector;
5
5
  let terminal;
@@ -363,3 +363,147 @@ describe('CodexStateDetector', () => {
363
363
  expect(state).toBe('waiting_input');
364
364
  });
365
365
  });
366
+ describe('CursorStateDetector', () => {
367
+ let detector;
368
+ let terminal;
369
+ const createMockTerminal = (lines) => {
370
+ const buffer = {
371
+ length: lines.length,
372
+ active: {
373
+ length: lines.length,
374
+ getLine: (index) => {
375
+ if (index >= 0 && index < lines.length) {
376
+ return {
377
+ translateToString: () => lines[index],
378
+ };
379
+ }
380
+ return null;
381
+ },
382
+ },
383
+ };
384
+ return { buffer };
385
+ };
386
+ beforeEach(() => {
387
+ detector = new CursorStateDetector();
388
+ });
389
+ it('should detect waiting_input state for (y) (enter) pattern', () => {
390
+ // Arrange
391
+ terminal = createMockTerminal([
392
+ 'Some output',
393
+ 'Apply changes? (y) (enter)',
394
+ '> ',
395
+ ]);
396
+ // Act
397
+ const state = detector.detectState(terminal, 'idle');
398
+ // Assert
399
+ expect(state).toBe('waiting_input');
400
+ });
401
+ it('should detect waiting_input state for (Y) (ENTER) pattern (case insensitive)', () => {
402
+ // Arrange
403
+ terminal = createMockTerminal([
404
+ 'Some output',
405
+ 'Continue? (Y) (ENTER)',
406
+ '> ',
407
+ ]);
408
+ // Act
409
+ const state = detector.detectState(terminal, 'idle');
410
+ // Assert
411
+ expect(state).toBe('waiting_input');
412
+ });
413
+ it('should detect waiting_input state for Keep (n) pattern', () => {
414
+ // Arrange
415
+ terminal = createMockTerminal([
416
+ 'Changes detected',
417
+ 'Keep (n) or replace?',
418
+ '> ',
419
+ ]);
420
+ // Act
421
+ const state = detector.detectState(terminal, 'idle');
422
+ // Assert
423
+ expect(state).toBe('waiting_input');
424
+ });
425
+ it('should detect waiting_input state for KEEP (N) pattern (case insensitive)', () => {
426
+ // Arrange
427
+ terminal = createMockTerminal([
428
+ 'Some output',
429
+ 'KEEP (N) current version?',
430
+ '> ',
431
+ ]);
432
+ // Act
433
+ const state = detector.detectState(terminal, 'idle');
434
+ // Assert
435
+ expect(state).toBe('waiting_input');
436
+ });
437
+ it('should detect waiting_input state for Auto pattern with shift+tab', () => {
438
+ // Arrange
439
+ terminal = createMockTerminal([
440
+ 'Some output',
441
+ 'Auto apply changes (shift+tab)',
442
+ '> ',
443
+ ]);
444
+ // Act
445
+ const state = detector.detectState(terminal, 'idle');
446
+ // Assert
447
+ expect(state).toBe('waiting_input');
448
+ });
449
+ it('should detect waiting_input state for AUTO with SHIFT+TAB (case insensitive)', () => {
450
+ // Arrange
451
+ terminal = createMockTerminal([
452
+ 'Some output',
453
+ 'AUTO COMPLETE (SHIFT+TAB)',
454
+ '> ',
455
+ ]);
456
+ // Act
457
+ const state = detector.detectState(terminal, 'idle');
458
+ // Assert
459
+ expect(state).toBe('waiting_input');
460
+ });
461
+ it('should detect busy state for ctrl+c to stop pattern', () => {
462
+ // Arrange
463
+ terminal = createMockTerminal([
464
+ 'Processing...',
465
+ 'Press ctrl+c to stop',
466
+ 'Working...',
467
+ ]);
468
+ // Act
469
+ const state = detector.detectState(terminal, 'idle');
470
+ // Assert
471
+ expect(state).toBe('busy');
472
+ });
473
+ it('should detect busy state for CTRL+C TO STOP (case insensitive)', () => {
474
+ // Arrange
475
+ terminal = createMockTerminal([
476
+ 'Running...',
477
+ 'PRESS CTRL+C TO STOP',
478
+ 'Processing...',
479
+ ]);
480
+ // Act
481
+ const state = detector.detectState(terminal, 'idle');
482
+ // Assert
483
+ expect(state).toBe('busy');
484
+ });
485
+ it('should detect idle state when no patterns match', () => {
486
+ // Arrange
487
+ terminal = createMockTerminal(['Normal output', 'Some message', 'Ready']);
488
+ // Act
489
+ const state = detector.detectState(terminal, 'idle');
490
+ // Assert
491
+ expect(state).toBe('idle');
492
+ });
493
+ it('should prioritize waiting_input over busy (Priority 1)', () => {
494
+ // Arrange
495
+ terminal = createMockTerminal(['ctrl+c to stop', '(y) (enter)']);
496
+ // Act
497
+ const state = detector.detectState(terminal, 'idle');
498
+ // Assert
499
+ expect(state).toBe('waiting_input'); // waiting_input should take precedence
500
+ });
501
+ it('should handle empty terminal', () => {
502
+ // Arrange
503
+ terminal = createMockTerminal([]);
504
+ // Act
505
+ const state = detector.detectState(terminal, 'idle');
506
+ // Assert
507
+ expect(state).toBe('idle');
508
+ });
509
+ });
@@ -3,7 +3,7 @@ import type pkg from '@xterm/headless';
3
3
  import { GitStatus } from '../utils/gitStatus.js';
4
4
  export type Terminal = InstanceType<typeof pkg.Terminal>;
5
5
  export type SessionState = 'idle' | 'busy' | 'waiting_input';
6
- export type StateDetectionStrategy = 'claude' | 'gemini' | 'codex';
6
+ export type StateDetectionStrategy = 'claude' | 'gemini' | 'codex' | 'cursor';
7
7
  export interface Worktree {
8
8
  path: string;
9
9
  branch?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",