ccmanager 3.2.10 → 3.3.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.
@@ -19,6 +19,7 @@ const createStrategyItems = () => {
19
19
  value: 'github-copilot',
20
20
  },
21
21
  cline: { label: 'Cline', value: 'cline' },
22
+ opencode: { label: 'OpenCode', value: 'opencode' },
22
23
  };
23
24
  return Object.values(strategies);
24
25
  };
@@ -35,6 +36,10 @@ const formatDetectionStrategy = (strategy) => {
35
36
  return 'Cursor';
36
37
  case 'github-copilot':
37
38
  return 'GitHub Copilot CLI';
39
+ case 'cline':
40
+ return 'Cline';
41
+ case 'opencode':
42
+ return 'OpenCode';
38
43
  default:
39
44
  return 'Claude';
40
45
  }
@@ -12,6 +12,7 @@ import { ProcessError, ConfigError } from '../types/errors.js';
12
12
  import { autoApprovalVerifier } from './autoApprovalVerifier.js';
13
13
  import { logger } from '../utils/logger.js';
14
14
  import { Mutex, createInitialSessionStateData } from '../utils/mutex.js';
15
+ import { getTerminalScreenContent } from '../utils/screenCapture.js';
15
16
  const { Terminal } = pkg;
16
17
  const execAsync = promisify(exec);
17
18
  const TERMINAL_CONTENT_MAX_LINES = 300;
@@ -41,20 +42,9 @@ export class SessionManager extends EventEmitter {
41
42
  return detectedState;
42
43
  }
43
44
  getTerminalContent(session) {
44
- const buffer = session.terminal.buffer.active;
45
- const lines = [];
46
- // Start from the bottom and work our way up
47
- for (let i = buffer.length - 1; i >= 0 && lines.length < TERMINAL_CONTENT_MAX_LINES; i--) {
48
- const line = buffer.getLine(i);
49
- if (line) {
50
- const text = line.translateToString(true);
51
- // Skip empty lines at the bottom
52
- if (lines.length > 0 || text.trim() !== '') {
53
- lines.unshift(text);
54
- }
55
- }
56
- }
57
- return lines.join('\n');
45
+ // Use the new screen capture utility that correctly handles
46
+ // both normal and alternate screen buffers
47
+ return getTerminalScreenContent(session.terminal, TERMINAL_CONTENT_MAX_LINES);
58
48
  }
59
49
  handleAutoApproval(session) {
60
50
  // Cancel any existing verification before starting a new one
@@ -4,6 +4,7 @@ import { CodexStateDetector } from './codex.js';
4
4
  import { CursorStateDetector } from './cursor.js';
5
5
  import { GitHubCopilotStateDetector } from './github-copilot.js';
6
6
  import { ClineStateDetector } from './cline.js';
7
+ import { OpenCodeStateDetector } from './opencode.js';
7
8
  export function createStateDetector(strategy = 'claude') {
8
9
  switch (strategy) {
9
10
  case 'claude':
@@ -18,6 +19,8 @@ export function createStateDetector(strategy = 'claude') {
18
19
  return new GitHubCopilotStateDetector();
19
20
  case 'cline':
20
21
  return new ClineStateDetector();
22
+ case 'opencode':
23
+ return new OpenCodeStateDetector();
21
24
  default:
22
25
  return new ClaudeStateDetector();
23
26
  }
@@ -0,0 +1,5 @@
1
+ import { SessionState, Terminal } from '../../types/index.js';
2
+ import { BaseStateDetector } from './base.js';
3
+ export declare class OpenCodeStateDetector extends BaseStateDetector {
4
+ detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ }
@@ -0,0 +1,17 @@
1
+ import { BaseStateDetector } from './base.js';
2
+ export class OpenCodeStateDetector extends BaseStateDetector {
3
+ detectState(terminal, _currentState) {
4
+ const content = this.getTerminalContent(terminal);
5
+ // Check for waiting input state - permission required prompt
6
+ // The triangle symbol (△) indicates permission is required
7
+ if (content.includes('△ Permission required')) {
8
+ return 'waiting_input';
9
+ }
10
+ // Check for busy state - "esc interrupt" pattern indicates active processing
11
+ if (/esc.*interrupt/i.test(content)) {
12
+ return 'busy';
13
+ }
14
+ // Otherwise idle
15
+ return 'idle';
16
+ }
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { OpenCodeStateDetector } from './opencode.js';
3
+ import { createMockTerminal } from './testUtils.js';
4
+ describe('OpenCodeStateDetector', () => {
5
+ let detector;
6
+ let terminal;
7
+ beforeEach(() => {
8
+ detector = new OpenCodeStateDetector();
9
+ });
10
+ it('should detect waiting_input state for "△ Permission required" pattern', () => {
11
+ // Arrange
12
+ terminal = createMockTerminal([
13
+ 'Some output',
14
+ '△ Permission required',
15
+ 'Press Enter to allow',
16
+ ]);
17
+ // Act
18
+ const state = detector.detectState(terminal, 'idle');
19
+ // Assert
20
+ expect(state).toBe('waiting_input');
21
+ });
22
+ it('should detect busy state for "esc interrupt" pattern', () => {
23
+ // Arrange
24
+ terminal = createMockTerminal([
25
+ 'Processing...',
26
+ 'Press esc to interrupt',
27
+ 'Working...',
28
+ ]);
29
+ // Act
30
+ const state = detector.detectState(terminal, 'idle');
31
+ // Assert
32
+ expect(state).toBe('busy');
33
+ });
34
+ it('should detect busy state for "ESC INTERRUPT" (uppercase)', () => {
35
+ // Arrange
36
+ terminal = createMockTerminal([
37
+ 'Processing...',
38
+ 'PRESS ESC TO INTERRUPT',
39
+ 'Working...',
40
+ ]);
41
+ // Act
42
+ const state = detector.detectState(terminal, 'idle');
43
+ // Assert
44
+ expect(state).toBe('busy');
45
+ });
46
+ it('should detect busy state for "Esc to interrupt" pattern', () => {
47
+ // Arrange
48
+ terminal = createMockTerminal(['Processing...', 'Esc to interrupt']);
49
+ // Act
50
+ const state = detector.detectState(terminal, 'idle');
51
+ // Assert
52
+ expect(state).toBe('busy');
53
+ });
54
+ it('should detect idle state when no patterns match', () => {
55
+ // Arrange
56
+ terminal = createMockTerminal(['Normal output', 'Some message', 'Ready']);
57
+ // Act
58
+ const state = detector.detectState(terminal, 'idle');
59
+ // Assert
60
+ expect(state).toBe('idle');
61
+ });
62
+ it('should prioritize waiting_input over busy when both patterns present', () => {
63
+ // Arrange
64
+ terminal = createMockTerminal([
65
+ 'esc to interrupt',
66
+ '△ Permission required',
67
+ ]);
68
+ // Act
69
+ const state = detector.detectState(terminal, 'idle');
70
+ // Assert
71
+ expect(state).toBe('waiting_input');
72
+ });
73
+ it('should detect waiting_input with full permission prompt', () => {
74
+ // Arrange
75
+ terminal = createMockTerminal([
76
+ 'opencode v0.1.0',
77
+ '',
78
+ '△ Permission required',
79
+ 'The AI wants to execute a shell command',
80
+ '',
81
+ 'Press Enter to allow, Esc to deny',
82
+ ]);
83
+ // Act
84
+ const state = detector.detectState(terminal, 'idle');
85
+ // Assert
86
+ expect(state).toBe('waiting_input');
87
+ });
88
+ });
@@ -4,7 +4,7 @@ import { GitStatus } from '../utils/gitStatus.js';
4
4
  import { Mutex, SessionStateData } from '../utils/mutex.js';
5
5
  export type Terminal = InstanceType<typeof pkg.Terminal>;
6
6
  export type SessionState = 'idle' | 'busy' | 'waiting_input' | 'pending_auto_approval';
7
- export type StateDetectionStrategy = 'claude' | 'gemini' | 'codex' | 'cursor' | 'github-copilot' | 'cline';
7
+ export type StateDetectionStrategy = 'claude' | 'gemini' | 'codex' | 'cursor' | 'github-copilot' | 'cline' | 'opencode';
8
8
  export interface Worktree {
9
9
  path: string;
10
10
  branch?: string;
@@ -0,0 +1,36 @@
1
+ import type { Terminal } from '@xterm/headless';
2
+ export interface ScreenState {
3
+ timestamp: string;
4
+ bufferType: 'normal' | 'alternate';
5
+ cursorX: number;
6
+ cursorY: number;
7
+ cols: number;
8
+ rows: number;
9
+ lines: string[];
10
+ }
11
+ /**
12
+ * Captures the current screen state of the terminal.
13
+ * This correctly handles both normal and alternate screen buffers,
14
+ * capturing only the visible screen content (not scrollback history).
15
+ *
16
+ * @param terminal - The xterm terminal instance
17
+ * @returns The current screen state including all visible lines
18
+ */
19
+ export declare function captureScreen(terminal: Terminal): ScreenState;
20
+ /**
21
+ * Formats the screen state into a human-readable string.
22
+ *
23
+ * @param state - The screen state to format
24
+ * @returns Formatted string representation of the screen state
25
+ */
26
+ export declare function formatScreenState(state: ScreenState): string;
27
+ /**
28
+ * Gets the terminal content as a single string.
29
+ * This is a convenience function that captures the screen and returns
30
+ * just the lines joined together.
31
+ *
32
+ * @param terminal - The xterm terminal instance
33
+ * @param maxLines - Optional maximum number of lines to return (from the bottom)
34
+ * @returns The terminal content as a string
35
+ */
36
+ export declare function getTerminalScreenContent(terminal: Terminal, maxLines?: number): string;
@@ -0,0 +1,70 @@
1
+ function lineToString(line, cols) {
2
+ if (!line) {
3
+ return '';
4
+ }
5
+ return line.translateToString(true, 0, cols);
6
+ }
7
+ /**
8
+ * Captures the current screen state of the terminal.
9
+ * This correctly handles both normal and alternate screen buffers,
10
+ * capturing only the visible screen content (not scrollback history).
11
+ *
12
+ * @param terminal - The xterm terminal instance
13
+ * @returns The current screen state including all visible lines
14
+ */
15
+ export function captureScreen(terminal) {
16
+ const buffer = terminal.buffer.active;
17
+ const lines = [];
18
+ // Capture only the visible screen area (terminal.rows lines)
19
+ // This works correctly for both normal and alternate screen buffers
20
+ for (let y = 0; y < terminal.rows; y++) {
21
+ const line = buffer.getLine(y);
22
+ lines.push(lineToString(line, terminal.cols));
23
+ }
24
+ return {
25
+ timestamp: new Date().toISOString(),
26
+ bufferType: buffer.type,
27
+ cursorX: buffer.cursorX,
28
+ cursorY: buffer.cursorY,
29
+ cols: terminal.cols,
30
+ rows: terminal.rows,
31
+ lines,
32
+ };
33
+ }
34
+ /**
35
+ * Formats the screen state into a human-readable string.
36
+ *
37
+ * @param state - The screen state to format
38
+ * @returns Formatted string representation of the screen state
39
+ */
40
+ export function formatScreenState(state) {
41
+ const separator = '-'.repeat(state.cols);
42
+ let output = `\n[${state.timestamp}] Buffer: ${state.bufferType} | Cursor: (${state.cursorX}, ${state.cursorY}) | Size: ${state.cols}x${state.rows}\n${separator}\n`;
43
+ for (const line of state.lines) {
44
+ output += line + '\n';
45
+ }
46
+ output += `${separator}\n\n`;
47
+ return output;
48
+ }
49
+ /**
50
+ * Gets the terminal content as a single string.
51
+ * This is a convenience function that captures the screen and returns
52
+ * just the lines joined together.
53
+ *
54
+ * @param terminal - The xterm terminal instance
55
+ * @param maxLines - Optional maximum number of lines to return (from the bottom)
56
+ * @returns The terminal content as a string
57
+ */
58
+ export function getTerminalScreenContent(terminal, maxLines) {
59
+ const state = captureScreen(terminal);
60
+ let lines = state.lines;
61
+ // Trim empty lines from the bottom
62
+ while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') {
63
+ lines.pop();
64
+ }
65
+ // If maxLines is specified, take only the last maxLines
66
+ if (maxLines !== undefined && lines.length > maxLines) {
67
+ lines = lines.slice(-maxLines);
68
+ }
69
+ return lines.join('\n');
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "3.2.10",
3
+ "version": "3.3.0",
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.10",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "3.2.10",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "3.2.10",
47
- "@kodaikabasawa/ccmanager-linux-x64": "3.2.10",
48
- "@kodaikabasawa/ccmanager-win32-x64": "3.2.10"
44
+ "@kodaikabasawa/ccmanager-darwin-arm64": "3.3.0",
45
+ "@kodaikabasawa/ccmanager-darwin-x64": "3.3.0",
46
+ "@kodaikabasawa/ccmanager-linux-arm64": "3.3.0",
47
+ "@kodaikabasawa/ccmanager-linux-x64": "3.3.0",
48
+ "@kodaikabasawa/ccmanager-win32-x64": "3.3.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.28.0",