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.
- package/dist/components/ConfigureCommand.js +5 -0
- package/dist/services/sessionManager.js +4 -14
- package/dist/services/stateDetector/index.js +3 -0
- package/dist/services/stateDetector/opencode.d.ts +5 -0
- package/dist/services/stateDetector/opencode.js +17 -0
- package/dist/services/stateDetector/opencode.test.d.ts +1 -0
- package/dist/services/stateDetector/opencode.test.js +88 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/screenCapture.d.ts +36 -0
- package/dist/utils/screenCapture.js +70 -0
- package/package.json +6 -6
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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,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
|
+
});
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "3.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "3.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "3.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "3.
|
|
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",
|