ccmanager 3.5.2 → 3.5.4
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/TextInputWrapper.d.ts +0 -5
- package/dist/components/TextInputWrapper.js +11 -138
- package/dist/services/sessionManager.d.ts +0 -1
- package/dist/services/sessionManager.js +6 -6
- package/dist/services/stateDetector/base.js +4 -15
- package/dist/services/stateDetector/testUtils.d.ts +5 -1
- package/dist/services/stateDetector/testUtils.js +6 -3
- package/package.json +7 -7
|
@@ -9,10 +9,5 @@ interface TextInputWrapperProps {
|
|
|
9
9
|
showCursor?: boolean;
|
|
10
10
|
highlightPastedText?: boolean;
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Custom text input component that handles rapid input correctly.
|
|
14
|
-
* This is a replacement for ink-text-input that uses refs for immediate
|
|
15
|
-
* state updates, which is necessary for text expansion tools like Espanso.
|
|
16
|
-
*/
|
|
17
12
|
declare const TextInputWrapper: React.FC<TextInputWrapperProps>;
|
|
18
13
|
export default TextInputWrapper;
|
|
@@ -1,142 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import
|
|
3
|
-
import { Text, useInput } from 'ink';
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import TextInput from 'ink-text-input';
|
|
4
3
|
import stripAnsi from 'strip-ansi';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const valueRef = useRef(value);
|
|
14
|
-
const cursorRef = useRef(value.length);
|
|
15
|
-
// State for triggering re-renders
|
|
16
|
-
const [, forceUpdate] = useState({});
|
|
17
|
-
// Sync refs when value prop changes from parent
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
valueRef.current = value;
|
|
20
|
-
// Adjust cursor if it's beyond the new value length
|
|
21
|
-
if (cursorRef.current > value.length) {
|
|
22
|
-
cursorRef.current = value.length;
|
|
23
|
-
}
|
|
24
|
-
}, [value]);
|
|
25
|
-
const cleanValue = (val) => {
|
|
26
|
-
let cleaned = stripAnsi(val);
|
|
27
|
-
cleaned = cleaned.replace(/\[200~/g, '').replace(/\[201~/g, '');
|
|
28
|
-
return cleaned;
|
|
4
|
+
const TextInputWrapper = ({ value, onChange, ...props }) => {
|
|
5
|
+
const handleChange = (newValue) => {
|
|
6
|
+
// First strip all ANSI escape sequences
|
|
7
|
+
let cleanedValue = stripAnsi(newValue);
|
|
8
|
+
// Then specifically remove bracketed paste mode markers that might remain
|
|
9
|
+
// These sometimes appear as literal text after ANSI stripping
|
|
10
|
+
cleanedValue = cleanedValue.replace(/\[200~/g, '').replace(/\[201~/g, '');
|
|
11
|
+
onChange(cleanedValue);
|
|
29
12
|
};
|
|
30
|
-
|
|
31
|
-
// This handles cases where text expansion tools send backspaces as characters
|
|
32
|
-
const processBackspaces = (input, currentValue, cursor) => {
|
|
33
|
-
let newValue = currentValue;
|
|
34
|
-
let newCursor = cursor;
|
|
35
|
-
let remaining = '';
|
|
36
|
-
for (let i = 0; i < input.length; i++) {
|
|
37
|
-
const char = input[i];
|
|
38
|
-
const charCode = char?.charCodeAt(0);
|
|
39
|
-
// Check for backspace characters (ASCII 8 or 127)
|
|
40
|
-
if (charCode === 8 || charCode === 127) {
|
|
41
|
-
if (newCursor > 0) {
|
|
42
|
-
newValue =
|
|
43
|
-
newValue.slice(0, newCursor - 1) + newValue.slice(newCursor);
|
|
44
|
-
newCursor--;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
// Regular character - add to remaining
|
|
49
|
-
remaining += char;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return { value: newValue, cursor: newCursor, remainingInput: remaining };
|
|
53
|
-
};
|
|
54
|
-
useInput((input, key) => {
|
|
55
|
-
// Ignore certain keys
|
|
56
|
-
if (key.upArrow ||
|
|
57
|
-
key.downArrow ||
|
|
58
|
-
(key.ctrl && input === 'c') ||
|
|
59
|
-
key.tab ||
|
|
60
|
-
(key.shift && key.tab)) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
// Handle Enter/Return
|
|
64
|
-
if (key.return) {
|
|
65
|
-
if (onSubmit) {
|
|
66
|
-
onSubmit(valueRef.current);
|
|
67
|
-
}
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
let currentValue = valueRef.current;
|
|
71
|
-
let cursor = cursorRef.current;
|
|
72
|
-
if (key.leftArrow) {
|
|
73
|
-
if (showCursor && cursor > 0) {
|
|
74
|
-
cursorRef.current = cursor - 1;
|
|
75
|
-
forceUpdate({});
|
|
76
|
-
}
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (key.rightArrow) {
|
|
80
|
-
if (showCursor && cursor < currentValue.length) {
|
|
81
|
-
cursorRef.current = cursor + 1;
|
|
82
|
-
forceUpdate({});
|
|
83
|
-
}
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (key.backspace || key.delete) {
|
|
87
|
-
if (cursor > 0) {
|
|
88
|
-
const nextValue = currentValue.slice(0, cursor - 1) + currentValue.slice(cursor);
|
|
89
|
-
valueRef.current = nextValue;
|
|
90
|
-
cursorRef.current = cursor - 1;
|
|
91
|
-
onChange(nextValue);
|
|
92
|
-
forceUpdate({});
|
|
93
|
-
}
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
// Process input that might contain embedded backspace characters
|
|
97
|
-
// (some text expansion tools send backspaces as part of the input string)
|
|
98
|
-
const { value: processedValue, cursor: processedCursor, remainingInput, } = processBackspaces(input, currentValue, cursor);
|
|
99
|
-
currentValue = processedValue;
|
|
100
|
-
cursor = processedCursor;
|
|
101
|
-
// Add remaining characters (non-backspace)
|
|
102
|
-
if (remainingInput) {
|
|
103
|
-
const cleanedInput = cleanValue(remainingInput);
|
|
104
|
-
if (cleanedInput) {
|
|
105
|
-
currentValue =
|
|
106
|
-
currentValue.slice(0, cursor) +
|
|
107
|
-
cleanedInput +
|
|
108
|
-
currentValue.slice(cursor);
|
|
109
|
-
cursor = cursor + cleanedInput.length;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// Update refs immediately (synchronously)
|
|
113
|
-
valueRef.current = currentValue;
|
|
114
|
-
cursorRef.current = cursor;
|
|
115
|
-
// Notify parent of value change
|
|
116
|
-
onChange(currentValue);
|
|
117
|
-
// Force re-render to update display
|
|
118
|
-
forceUpdate({});
|
|
119
|
-
}, { isActive: focus });
|
|
120
|
-
// Render the text with cursor
|
|
121
|
-
const displayValue = mask
|
|
122
|
-
? mask.repeat(valueRef.current.length)
|
|
123
|
-
: valueRef.current;
|
|
124
|
-
const cursor = cursorRef.current;
|
|
125
|
-
if (!showCursor || !focus) {
|
|
126
|
-
return (_jsx(Text, { children: displayValue.length > 0 ? (displayValue) : placeholder ? (_jsx(Text, { dimColor: true, children: placeholder })) : null }));
|
|
127
|
-
}
|
|
128
|
-
// Show cursor
|
|
129
|
-
if (displayValue.length === 0) {
|
|
130
|
-
// Show placeholder with cursor on first char
|
|
131
|
-
if (placeholder) {
|
|
132
|
-
return (_jsxs(Text, { children: [_jsx(Text, { inverse: true, children: placeholder[0] || ' ' }), _jsx(Text, { dimColor: true, children: placeholder.slice(1) })] }));
|
|
133
|
-
}
|
|
134
|
-
return _jsx(Text, { inverse: true, children: " " });
|
|
135
|
-
}
|
|
136
|
-
// Render value with cursor
|
|
137
|
-
const beforeCursor = displayValue.slice(0, cursor);
|
|
138
|
-
const atCursor = displayValue[cursor] || ' ';
|
|
139
|
-
const afterCursor = displayValue.slice(cursor + 1);
|
|
140
|
-
return (_jsxs(Text, { children: [beforeCursor, _jsx(Text, { inverse: true, children: atCursor }), afterCursor] }));
|
|
13
|
+
return _jsx(TextInput, { value: value, onChange: handleChange, ...props });
|
|
141
14
|
};
|
|
142
15
|
export default TextInputWrapper;
|
|
@@ -17,7 +17,6 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
|
|
|
17
17
|
private spawn;
|
|
18
18
|
detectTerminalState(session: Session): SessionState;
|
|
19
19
|
detectBackgroundTask(session: Session): number;
|
|
20
|
-
private getTerminalContent;
|
|
21
20
|
private handleAutoApproval;
|
|
22
21
|
private cancelAutoApprovalVerification;
|
|
23
22
|
/**
|
|
@@ -46,11 +46,6 @@ export class SessionManager extends EventEmitter {
|
|
|
46
46
|
detectBackgroundTask(session) {
|
|
47
47
|
return session.stateDetector.detectBackgroundTask(session.terminal);
|
|
48
48
|
}
|
|
49
|
-
getTerminalContent(session) {
|
|
50
|
-
// Use the new screen capture utility that correctly handles
|
|
51
|
-
// both normal and alternate screen buffers
|
|
52
|
-
return getTerminalScreenContent(session.terminal, TERMINAL_CONTENT_MAX_LINES);
|
|
53
|
-
}
|
|
54
49
|
handleAutoApproval(session) {
|
|
55
50
|
// Cancel any existing verification before starting a new one
|
|
56
51
|
this.cancelAutoApprovalVerification(session, 'Restarting verification for pending auto-approval state');
|
|
@@ -61,7 +56,7 @@ export class SessionManager extends EventEmitter {
|
|
|
61
56
|
autoApprovalReason: undefined,
|
|
62
57
|
}));
|
|
63
58
|
// Get terminal content for verification
|
|
64
|
-
const terminalContent =
|
|
59
|
+
const terminalContent = getTerminalScreenContent(session.terminal, TERMINAL_CONTENT_MAX_LINES);
|
|
65
60
|
// Verify if permission is needed
|
|
66
61
|
void Effect.runPromise(autoApprovalVerifier.verifyNeedsPermission(terminalContent, {
|
|
67
62
|
signal: abortController.signal,
|
|
@@ -277,6 +272,11 @@ export class SessionManager extends EventEmitter {
|
|
|
277
272
|
session.process.onData((data) => {
|
|
278
273
|
// Write data to virtual terminal
|
|
279
274
|
session.terminal.write(data);
|
|
275
|
+
// Check for screen clear escape sequence (e.g., from /clear command)
|
|
276
|
+
// When detected, clear the output history to prevent replaying old content on restore
|
|
277
|
+
if (data.includes('\x1B[2J')) {
|
|
278
|
+
session.outputHistory = [];
|
|
279
|
+
}
|
|
280
280
|
// Store in output history as Buffer
|
|
281
281
|
const buffer = Buffer.from(data, 'utf8');
|
|
282
282
|
session.outputHistory.push(buffer);
|
|
@@ -1,21 +1,10 @@
|
|
|
1
|
+
import { getTerminalScreenContent } from '../../utils/screenCapture.js';
|
|
1
2
|
export class BaseStateDetector {
|
|
2
3
|
getTerminalLines(terminal, maxLines = 30) {
|
|
3
|
-
const
|
|
4
|
-
|
|
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;
|
|
4
|
+
const content = getTerminalScreenContent(terminal, maxLines);
|
|
5
|
+
return content.split('\n');
|
|
17
6
|
}
|
|
18
7
|
getTerminalContent(terminal, maxLines = 30) {
|
|
19
|
-
return
|
|
8
|
+
return getTerminalScreenContent(terminal, maxLines);
|
|
20
9
|
}
|
|
21
10
|
}
|
|
@@ -2,6 +2,10 @@ import type { Terminal } from '../../types/index.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Creates a mock Terminal object for testing state detectors.
|
|
4
4
|
* @param lines - Array of strings representing terminal output lines
|
|
5
|
+
* @param options - Optional configuration for rows and cols
|
|
5
6
|
* @returns Mock Terminal object with buffer interface
|
|
6
7
|
*/
|
|
7
|
-
export declare const createMockTerminal: (lines: string[]
|
|
8
|
+
export declare const createMockTerminal: (lines: string[], options?: {
|
|
9
|
+
rows?: number;
|
|
10
|
+
cols?: number;
|
|
11
|
+
}) => Terminal;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Creates a mock Terminal object for testing state detectors.
|
|
3
3
|
* @param lines - Array of strings representing terminal output lines
|
|
4
|
+
* @param options - Optional configuration for rows and cols
|
|
4
5
|
* @returns Mock Terminal object with buffer interface
|
|
5
6
|
*/
|
|
6
|
-
export const createMockTerminal = (lines) => {
|
|
7
|
+
export const createMockTerminal = (lines, options) => {
|
|
8
|
+
const rows = options?.rows ?? lines.length;
|
|
9
|
+
const cols = options?.cols ?? 80;
|
|
7
10
|
const buffer = {
|
|
8
11
|
length: lines.length,
|
|
9
12
|
active: {
|
|
@@ -11,12 +14,12 @@ export const createMockTerminal = (lines) => {
|
|
|
11
14
|
getLine: (index) => {
|
|
12
15
|
if (index >= 0 && index < lines.length) {
|
|
13
16
|
return {
|
|
14
|
-
translateToString: () => lines[index],
|
|
17
|
+
translateToString: (_trimRight, _startCol, _endCol) => lines[index],
|
|
15
18
|
};
|
|
16
19
|
}
|
|
17
20
|
return null;
|
|
18
21
|
},
|
|
19
22
|
},
|
|
20
23
|
};
|
|
21
|
-
return { buffer };
|
|
24
|
+
return { buffer, rows, cols };
|
|
22
25
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.4",
|
|
4
4
|
"description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kodai Kabasawa",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"build:binary:native": "bun run scripts/build-binaries.ts --target=native",
|
|
27
27
|
"build:binary:all": "bun run scripts/build-binaries.ts --target=all",
|
|
28
28
|
"dev": "bun run tsc --watch",
|
|
29
|
-
"start": "bun dist/cli.js",
|
|
29
|
+
"start": "bun --no-env-file dist/cli.js",
|
|
30
30
|
"test": "vitest --run",
|
|
31
31
|
"lint": "bun run eslint src",
|
|
32
32
|
"lint:fix": "bun run eslint src --fix",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"bin"
|
|
42
42
|
],
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@kodaikabasawa/ccmanager-darwin-arm64": "3.5.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "3.5.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "3.5.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "3.5.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "3.5.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "3.5.4",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.5.4",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.5.4",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.5.4",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.5.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|