ccmanager 3.2.10 → 3.3.1
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 +12 -11
- package/dist/components/ConfigureCommand.js +71 -40
- 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 +8 -8
package/README.md
CHANGED
|
@@ -112,23 +112,24 @@ Note: Shortcuts from `shortcuts.json` will be automatically migrated to `config.
|
|
|
112
112
|
|
|
113
113
|
## Supported AI Assistants
|
|
114
114
|
|
|
115
|
-
CCManager
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
CCManager supports multiple AI coding assistants with tailored state detection for each:
|
|
116
|
+
|
|
117
|
+
| Assistant | Command | Installation |
|
|
118
|
+
|-----------|---------|--------------|
|
|
119
|
+
| Claude Code (Default) | `claude` | [code.claude.com](https://code.claude.com/docs/en/setup) |
|
|
120
|
+
| Gemini CLI | `gemini` | [github.com/google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli) |
|
|
121
|
+
| Codex CLI | `codex` | [github.com/openai/codex](https://github.com/openai/codex) |
|
|
122
|
+
| Cursor Agent | `cursor-agent` | [cursor.com/cli](https://cursor.com/docs/cli/overview) |
|
|
123
|
+
| Copilot CLI | `copilot` | [github.com/github/copilot-cli](https://github.com/github/copilot-cli) |
|
|
124
|
+
| Cline CLI | `cline` | [github.com/cline/cline](https://github.com/cline/cline) |
|
|
125
|
+
| OpenCode | `opencode` | [opencode.ai/docs](https://opencode.ai/docs) |
|
|
125
126
|
|
|
126
127
|
Each assistant has its own state detection strategy to properly track:
|
|
127
128
|
- **Idle**: Ready for new input
|
|
128
129
|
- **Busy**: Processing a request
|
|
129
130
|
- **Waiting**: Awaiting user confirmation
|
|
130
131
|
|
|
131
|
-
See [Gemini Support Documentation](docs/gemini-support.md) for
|
|
132
|
+
See [Gemini Support Documentation](docs/gemini-support.md) for CCManager-specific configuration.
|
|
132
133
|
|
|
133
134
|
|
|
134
135
|
## Command Configuration
|
|
@@ -19,11 +19,22 @@ 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
|
};
|
|
25
26
|
// Type-safe strategy items that ensures all StateDetectionStrategy values are included
|
|
26
27
|
const ALL_STRATEGY_ITEMS = createStrategyItems();
|
|
28
|
+
// Default command mapping for each strategy
|
|
29
|
+
const DEFAULT_COMMANDS = {
|
|
30
|
+
claude: 'claude',
|
|
31
|
+
gemini: 'gemini',
|
|
32
|
+
codex: 'codex',
|
|
33
|
+
cursor: 'cursor',
|
|
34
|
+
'github-copilot': 'copilot',
|
|
35
|
+
cline: 'cline',
|
|
36
|
+
opencode: 'opencode',
|
|
37
|
+
};
|
|
27
38
|
const formatDetectionStrategy = (strategy) => {
|
|
28
39
|
const value = strategy || 'claude';
|
|
29
40
|
switch (value) {
|
|
@@ -35,6 +46,10 @@ const formatDetectionStrategy = (strategy) => {
|
|
|
35
46
|
return 'Cursor';
|
|
36
47
|
case 'github-copilot':
|
|
37
48
|
return 'GitHub Copilot CLI';
|
|
49
|
+
case 'cline':
|
|
50
|
+
return 'Cline';
|
|
51
|
+
case 'opencode':
|
|
52
|
+
return 'OpenCode';
|
|
38
53
|
default:
|
|
39
54
|
return 'Claude';
|
|
40
55
|
}
|
|
@@ -52,7 +67,7 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
52
67
|
const [isSelectingStrategy, setIsSelectingStrategy] = useState(false);
|
|
53
68
|
const [isSelectingStrategyInAdd, setIsSelectingStrategyInAdd] = useState(false);
|
|
54
69
|
const [newPreset, setNewPreset] = useState({});
|
|
55
|
-
const [addStep, setAddStep] = useState('
|
|
70
|
+
const [addStep, setAddStep] = useState('detectionStrategy');
|
|
56
71
|
const [errorMessage, setErrorMessage] = useState(null);
|
|
57
72
|
// Remove handleListNavigation as SelectInput handles navigation internally
|
|
58
73
|
// Remove handleListSelection as we now use handleSelectItem
|
|
@@ -137,19 +152,8 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
137
152
|
};
|
|
138
153
|
const handleAddPresetInput = (value) => {
|
|
139
154
|
switch (addStep) {
|
|
140
|
-
case 'name':
|
|
141
|
-
// Prevent using "Default" as a name to avoid confusion
|
|
142
|
-
if (value.trim().toLowerCase() === 'default') {
|
|
143
|
-
setErrorMessage('Cannot use "Default" as a preset name. Please choose a different name.');
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
setNewPreset({ ...newPreset, name: value });
|
|
147
|
-
setAddStep('command');
|
|
148
|
-
setInputValue('');
|
|
149
|
-
setErrorMessage(null);
|
|
150
|
-
break;
|
|
151
155
|
case 'command':
|
|
152
|
-
setNewPreset({ ...newPreset, command: value
|
|
156
|
+
setNewPreset({ ...newPreset, command: value });
|
|
153
157
|
setAddStep('args');
|
|
154
158
|
setInputValue('');
|
|
155
159
|
break;
|
|
@@ -165,8 +169,38 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
165
169
|
? value.trim().split(/\s+/)
|
|
166
170
|
: undefined;
|
|
167
171
|
setNewPreset({ ...newPreset, fallbackArgs });
|
|
172
|
+
setAddStep('name');
|
|
173
|
+
setInputValue('');
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case 'name': {
|
|
177
|
+
if (!value.trim()) {
|
|
178
|
+
setErrorMessage('Preset name cannot be empty. Please enter a name.');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (value.trim().toLowerCase() === 'default') {
|
|
182
|
+
setErrorMessage('Cannot use "Default" as a preset name. Please choose a different name.');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const id = Date.now().toString();
|
|
186
|
+
const completePreset = {
|
|
187
|
+
id,
|
|
188
|
+
name: value,
|
|
189
|
+
command: newPreset.command || 'claude',
|
|
190
|
+
args: newPreset.args,
|
|
191
|
+
fallbackArgs: newPreset.fallbackArgs,
|
|
192
|
+
detectionStrategy: newPreset.detectionStrategy || 'claude',
|
|
193
|
+
};
|
|
194
|
+
const updatedPresets = [...presets, completePreset];
|
|
195
|
+
setPresets(updatedPresets);
|
|
196
|
+
configurationManager.addPreset(completePreset);
|
|
197
|
+
setViewMode('list');
|
|
198
|
+
setSelectedIndex(updatedPresets.length - 1);
|
|
199
|
+
setNewPreset({});
|
|
168
200
|
setAddStep('detectionStrategy');
|
|
169
|
-
|
|
201
|
+
setInputValue('');
|
|
202
|
+
setIsSelectingStrategyInAdd(false);
|
|
203
|
+
setErrorMessage(null);
|
|
170
204
|
break;
|
|
171
205
|
}
|
|
172
206
|
}
|
|
@@ -183,25 +217,16 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
183
217
|
setIsSelectingStrategy(false);
|
|
184
218
|
};
|
|
185
219
|
const handleAddStrategySelect = (item) => {
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const updatedPresets = [...presets, completePreset];
|
|
196
|
-
setPresets(updatedPresets);
|
|
197
|
-
configurationManager.addPreset(completePreset);
|
|
198
|
-
setViewMode('list');
|
|
199
|
-
setSelectedIndex(updatedPresets.length - 1);
|
|
200
|
-
setNewPreset({});
|
|
201
|
-
setAddStep('name');
|
|
202
|
-
setInputValue('');
|
|
220
|
+
const strategy = item.value;
|
|
221
|
+
const defaultCommand = DEFAULT_COMMANDS[strategy];
|
|
222
|
+
setNewPreset({
|
|
223
|
+
...newPreset,
|
|
224
|
+
detectionStrategy: strategy,
|
|
225
|
+
command: defaultCommand,
|
|
226
|
+
});
|
|
227
|
+
setAddStep('command');
|
|
228
|
+
setInputValue(defaultCommand);
|
|
203
229
|
setIsSelectingStrategyInAdd(false);
|
|
204
|
-
setErrorMessage(null);
|
|
205
230
|
};
|
|
206
231
|
const handleDeleteConfirm = () => {
|
|
207
232
|
if (selectedIndex === 0) {
|
|
@@ -234,7 +259,7 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
234
259
|
else if (isSelectingStrategyInAdd) {
|
|
235
260
|
setIsSelectingStrategyInAdd(false);
|
|
236
261
|
setViewMode('list');
|
|
237
|
-
setAddStep('
|
|
262
|
+
setAddStep('detectionStrategy');
|
|
238
263
|
setNewPreset({});
|
|
239
264
|
}
|
|
240
265
|
else if (editField) {
|
|
@@ -333,6 +358,8 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
333
358
|
React.createElement(Text, { bold: true, color: "green" }, "Add New Preset - Detection Strategy")),
|
|
334
359
|
React.createElement(Box, { marginBottom: 1 },
|
|
335
360
|
React.createElement(Text, null, "Choose the state detection strategy for this preset:")),
|
|
361
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
362
|
+
React.createElement(Text, { dimColor: true }, "The command will be auto-set based on the strategy (can be changed later)")),
|
|
336
363
|
React.createElement(SelectInput, { items: strategyItems, onSelect: handleAddStrategySelect, initialIndex: 0 }),
|
|
337
364
|
React.createElement(Box, { marginTop: 1 },
|
|
338
365
|
React.createElement(Text, { dimColor: true },
|
|
@@ -342,23 +369,26 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
342
369
|
" to cancel"))));
|
|
343
370
|
}
|
|
344
371
|
const titles = {
|
|
345
|
-
|
|
346
|
-
command: 'Enter command (
|
|
372
|
+
detectionStrategy: 'Select detection strategy:',
|
|
373
|
+
command: 'Enter command (default set by strategy, can be modified):',
|
|
347
374
|
args: 'Enter command arguments (space-separated):',
|
|
348
375
|
fallbackArgs: 'Enter fallback arguments (space-separated):',
|
|
376
|
+
name: 'Enter preset name (freely customizable):',
|
|
349
377
|
};
|
|
350
378
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
351
379
|
React.createElement(Box, { marginBottom: 1 },
|
|
352
380
|
React.createElement(Text, { bold: true, color: "green" }, "Add New Preset")),
|
|
353
381
|
React.createElement(Box, { marginBottom: 1 },
|
|
354
382
|
React.createElement(Text, null, titles[addStep])),
|
|
383
|
+
addStep === 'command' && (React.createElement(Box, { marginBottom: 1 },
|
|
384
|
+
React.createElement(Text, { dimColor: true }, "Auto-filled from your strategy selection. You can change this if needed."))),
|
|
355
385
|
errorMessage && (React.createElement(Box, { marginBottom: 1 },
|
|
356
386
|
React.createElement(Text, { color: "red" }, errorMessage))),
|
|
357
387
|
React.createElement(Box, null,
|
|
358
|
-
React.createElement(TextInputWrapper, { value: inputValue, onChange: setInputValue, onSubmit: handleAddPresetInput, placeholder: addStep === '
|
|
359
|
-
? 'e.g.,
|
|
360
|
-
: addStep === '
|
|
361
|
-
? 'e.g.,
|
|
388
|
+
React.createElement(TextInputWrapper, { value: inputValue, onChange: setInputValue, onSubmit: handleAddPresetInput, placeholder: addStep === 'name'
|
|
389
|
+
? 'e.g., Development'
|
|
390
|
+
: addStep === 'args' || addStep === 'fallbackArgs'
|
|
391
|
+
? 'e.g., --resume or leave empty'
|
|
362
392
|
: '' })),
|
|
363
393
|
React.createElement(Box, { marginTop: 1 },
|
|
364
394
|
React.createElement(Text, { dimColor: true },
|
|
@@ -489,7 +519,8 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
489
519
|
// Add New Preset
|
|
490
520
|
setViewMode('add');
|
|
491
521
|
setNewPreset({});
|
|
492
|
-
setAddStep('
|
|
522
|
+
setAddStep('detectionStrategy');
|
|
523
|
+
setIsSelectingStrategyInAdd(true);
|
|
493
524
|
setInputValue('');
|
|
494
525
|
}
|
|
495
526
|
else if (item.value === 'exit') {
|
|
@@ -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.1",
|
|
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.1",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.3.1",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.3.1",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.3.1",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.3.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|
|
@@ -68,14 +68,14 @@
|
|
|
68
68
|
},
|
|
69
69
|
"prettier": "@vdemedes/prettier-config",
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@xterm/headless": "^
|
|
71
|
+
"@xterm/headless": "^6.0.0",
|
|
72
72
|
"effect": "^3.18.2",
|
|
73
73
|
"ink": "5.2.1",
|
|
74
74
|
"ink-select-input": "^6.0.0",
|
|
75
75
|
"ink-text-input": "^6.0.0",
|
|
76
76
|
"meow": "^11.0.0",
|
|
77
77
|
"react": "18.3.1",
|
|
78
|
-
"react-devtools-core": "^
|
|
78
|
+
"react-devtools-core": "^7.0.1",
|
|
79
79
|
"react-dom": "18.3.1",
|
|
80
80
|
"strip-ansi": "^7.1.0"
|
|
81
81
|
}
|