ccmanager 3.11.0 → 3.11.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/dist/components/App.js +8 -5
- package/dist/components/TextInputWrapper.d.ts +0 -3
- package/dist/components/TextInputWrapper.js +120 -11
- package/dist/services/sessionManager.test.js +0 -12
- package/dist/services/worktreeNameGenerator.js +9 -1
- package/dist/utils/presetPrompt.js +7 -5
- package/dist/utils/presetPrompt.test.js +2 -14
- package/package.json +6 -6
package/dist/components/App.js
CHANGED
|
@@ -53,26 +53,24 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
55
|
// Helper function to create session with Effect-based error handling
|
|
56
|
-
const createSessionWithEffect = async (worktreePath, presetId, initialPrompt) => {
|
|
56
|
+
const createSessionWithEffect = useCallback(async (worktreePath, presetId, initialPrompt) => {
|
|
57
57
|
const sessionEffect = devcontainerConfig
|
|
58
58
|
? sessionManager.createSessionWithDevcontainerEffect(worktreePath, devcontainerConfig, presetId, initialPrompt)
|
|
59
59
|
: sessionManager.createSessionWithPresetEffect(worktreePath, presetId, initialPrompt);
|
|
60
60
|
// Execute the Effect and handle both success and failure cases
|
|
61
61
|
const result = await Effect.runPromise(Effect.either(sessionEffect));
|
|
62
62
|
if (result._tag === 'Left') {
|
|
63
|
-
// Handle error using pattern matching on _tag
|
|
64
63
|
const errorMessage = formatErrorMessage(result.left);
|
|
65
64
|
return {
|
|
66
65
|
success: false,
|
|
67
66
|
errorMessage: `Failed to create session: ${errorMessage}`,
|
|
68
67
|
};
|
|
69
68
|
}
|
|
70
|
-
// Success case - extract session from Right
|
|
71
69
|
return {
|
|
72
70
|
success: true,
|
|
73
71
|
session: result.right,
|
|
74
72
|
};
|
|
75
|
-
};
|
|
73
|
+
}, [sessionManager, devcontainerConfig]);
|
|
76
74
|
// Helper function to clear terminal screen
|
|
77
75
|
const clearScreen = () => {
|
|
78
76
|
if (process.stdout.isTTY) {
|
|
@@ -115,7 +113,12 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
115
113
|
session = result.session;
|
|
116
114
|
}
|
|
117
115
|
navigateToSession(session);
|
|
118
|
-
}, [
|
|
116
|
+
}, [
|
|
117
|
+
sessionManager,
|
|
118
|
+
navigateWithClear,
|
|
119
|
+
navigateToSession,
|
|
120
|
+
createSessionWithEffect,
|
|
121
|
+
]);
|
|
119
122
|
useEffect(() => {
|
|
120
123
|
// Listen for session exits to return to menu automatically
|
|
121
124
|
const handleSessionExit = (session) => {
|
|
@@ -5,9 +5,6 @@ interface TextInputWrapperProps {
|
|
|
5
5
|
onSubmit?: (value: string) => void;
|
|
6
6
|
placeholder?: string;
|
|
7
7
|
focus?: boolean;
|
|
8
|
-
mask?: string;
|
|
9
|
-
showCursor?: boolean;
|
|
10
|
-
highlightPastedText?: boolean;
|
|
11
8
|
}
|
|
12
9
|
declare const TextInputWrapper: React.FC<TextInputWrapperProps>;
|
|
13
10
|
export default TextInputWrapper;
|
|
@@ -1,15 +1,124 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
2
|
+
import { useReducer, useEffect, useRef, useMemo, } from 'react';
|
|
3
|
+
import { Text, useInput } from 'ink';
|
|
4
|
+
import chalk from 'chalk';
|
|
3
5
|
import stripAnsi from 'strip-ansi';
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
const reducer = (state, action) => {
|
|
7
|
+
switch (action.type) {
|
|
8
|
+
case 'move-cursor-left': {
|
|
9
|
+
return {
|
|
10
|
+
...state,
|
|
11
|
+
cursorOffset: Math.max(0, state.cursorOffset - 1),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
case 'move-cursor-right': {
|
|
15
|
+
return {
|
|
16
|
+
...state,
|
|
17
|
+
cursorOffset: Math.min(state.value.length, state.cursorOffset + 1),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
case 'insert': {
|
|
21
|
+
return {
|
|
22
|
+
value: state.value.slice(0, state.cursorOffset) +
|
|
23
|
+
action.text +
|
|
24
|
+
state.value.slice(state.cursorOffset),
|
|
25
|
+
cursorOffset: state.cursorOffset + action.text.length,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
case 'delete': {
|
|
29
|
+
if (state.cursorOffset === 0)
|
|
30
|
+
return state;
|
|
31
|
+
const newOffset = state.cursorOffset - 1;
|
|
32
|
+
return {
|
|
33
|
+
value: state.value.slice(0, newOffset) + state.value.slice(newOffset + 1),
|
|
34
|
+
cursorOffset: newOffset,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
case 'set': {
|
|
38
|
+
return {
|
|
39
|
+
value: action.value,
|
|
40
|
+
cursorOffset: action.value.length,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function cleanInput(input) {
|
|
46
|
+
let cleaned = stripAnsi(input);
|
|
47
|
+
cleaned = cleaned.replace(/\[200~/g, '').replace(/\[201~/g, '');
|
|
48
|
+
return cleaned;
|
|
49
|
+
}
|
|
50
|
+
const cursor = chalk.inverse(' ');
|
|
51
|
+
const TextInputWrapper = ({ value, onChange, onSubmit, placeholder = '', focus = true, }) => {
|
|
52
|
+
const [state, dispatch] = useReducer(reducer, {
|
|
53
|
+
value,
|
|
54
|
+
cursorOffset: value.length,
|
|
55
|
+
});
|
|
56
|
+
const lastReportedValue = useRef(value);
|
|
57
|
+
// Sync external value changes into internal state
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (value !== lastReportedValue.current) {
|
|
60
|
+
lastReportedValue.current = value;
|
|
61
|
+
dispatch({ type: 'set', value });
|
|
62
|
+
}
|
|
63
|
+
}, [value]);
|
|
64
|
+
// Report internal state changes to parent
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (state.value !== lastReportedValue.current) {
|
|
67
|
+
lastReportedValue.current = state.value;
|
|
68
|
+
onChange(state.value);
|
|
69
|
+
}
|
|
70
|
+
}, [state.value, onChange]);
|
|
71
|
+
useInput((input, key) => {
|
|
72
|
+
if (key.upArrow ||
|
|
73
|
+
key.downArrow ||
|
|
74
|
+
(key.ctrl && input === 'c') ||
|
|
75
|
+
key.tab ||
|
|
76
|
+
(key.shift && key.tab)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (key.return) {
|
|
80
|
+
onSubmit?.(state.value);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (key.leftArrow) {
|
|
84
|
+
dispatch({ type: 'move-cursor-left' });
|
|
85
|
+
}
|
|
86
|
+
else if (key.rightArrow) {
|
|
87
|
+
dispatch({ type: 'move-cursor-right' });
|
|
88
|
+
}
|
|
89
|
+
else if (key.backspace || key.delete) {
|
|
90
|
+
dispatch({ type: 'delete' });
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const cleaned = cleanInput(input);
|
|
94
|
+
if (cleaned) {
|
|
95
|
+
dispatch({ type: 'insert', text: cleaned });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}, { isActive: focus });
|
|
99
|
+
const renderedPlaceholder = useMemo(() => {
|
|
100
|
+
if (!focus) {
|
|
101
|
+
return placeholder ? chalk.dim(placeholder) : '';
|
|
102
|
+
}
|
|
103
|
+
return placeholder.length > 0
|
|
104
|
+
? chalk.inverse(placeholder[0]) + chalk.dim(placeholder.slice(1))
|
|
105
|
+
: cursor;
|
|
106
|
+
}, [focus, placeholder]);
|
|
107
|
+
const renderedValue = useMemo(() => {
|
|
108
|
+
if (!focus) {
|
|
109
|
+
return state.value;
|
|
110
|
+
}
|
|
111
|
+
let result = state.value.length > 0 ? '' : cursor;
|
|
112
|
+
let index = 0;
|
|
113
|
+
for (const char of state.value) {
|
|
114
|
+
result += index === state.cursorOffset ? chalk.inverse(char) : char;
|
|
115
|
+
index++;
|
|
116
|
+
}
|
|
117
|
+
if (state.value.length > 0 && state.cursorOffset === state.value.length) {
|
|
118
|
+
result += cursor;
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}, [focus, state.value, state.cursorOffset]);
|
|
122
|
+
return (_jsx(Text, { children: state.value.length > 0 ? renderedValue : renderedPlaceholder }));
|
|
14
123
|
};
|
|
15
124
|
export default TextInputWrapper;
|
|
@@ -164,18 +164,6 @@ describe('SessionManager', () => {
|
|
|
164
164
|
expect(spawn).toHaveBeenCalledWith('opencode', ['run', '--prompt', 'implement prompt flow'], expect.any(Object));
|
|
165
165
|
expect(mockPty.write).not.toHaveBeenCalled();
|
|
166
166
|
});
|
|
167
|
-
it('writes the initial prompt to stdin for unknown commands', async () => {
|
|
168
|
-
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
169
|
-
id: '1',
|
|
170
|
-
name: 'Custom',
|
|
171
|
-
command: 'custom-agent',
|
|
172
|
-
args: ['--interactive'],
|
|
173
|
-
});
|
|
174
|
-
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
175
|
-
await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree', undefined, 'implement prompt flow'));
|
|
176
|
-
expect(spawn).toHaveBeenCalledWith('custom-agent', ['--interactive'], expect.any(Object));
|
|
177
|
-
expect(mockPty.write).toHaveBeenCalledWith('implement prompt flow\r');
|
|
178
|
-
});
|
|
179
167
|
it('should fall back to default preset if specified preset not found', async () => {
|
|
180
168
|
// Setup mocks
|
|
181
169
|
vi.mocked(configReader.getPresetByIdEffect).mockReturnValue(Either.left(new ValidationError({
|
|
@@ -141,7 +141,15 @@ export class WorktreeNameGenerator {
|
|
|
141
141
|
reject(new Error('Timed out while generating a branch name with `claude -p`'));
|
|
142
142
|
});
|
|
143
143
|
}, DEFAULT_TIMEOUT_MS);
|
|
144
|
-
child = execFile('claude', [
|
|
144
|
+
child = execFile('claude', [
|
|
145
|
+
'-p',
|
|
146
|
+
'--model',
|
|
147
|
+
'haiku',
|
|
148
|
+
'--output-format',
|
|
149
|
+
'json',
|
|
150
|
+
'--json-schema',
|
|
151
|
+
JSON_SCHEMA,
|
|
152
|
+
], {
|
|
145
153
|
encoding: 'utf8',
|
|
146
154
|
maxBuffer: 1024 * 1024,
|
|
147
155
|
}, (error, stdout) => {
|
|
@@ -13,14 +13,16 @@ const PROMPT_FLAG = {
|
|
|
13
13
|
'github-copilot': '-i',
|
|
14
14
|
kimi: '-p',
|
|
15
15
|
};
|
|
16
|
+
const DEFAULT_DETECTION_STRATEGY = 'claude';
|
|
16
17
|
export const getPromptInjectionMethod = (preset) => {
|
|
17
|
-
|
|
18
|
+
const strategy = preset.detectionStrategy ?? DEFAULT_DETECTION_STRATEGY;
|
|
19
|
+
if (PROMPT_FLAG[strategy]) {
|
|
18
20
|
return 'flag';
|
|
19
21
|
}
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
if (strategy === 'claude' ||
|
|
23
|
+
strategy === 'codex' ||
|
|
24
|
+
strategy === 'cursor' ||
|
|
25
|
+
strategy === 'cline') {
|
|
24
26
|
return 'final-arg';
|
|
25
27
|
}
|
|
26
28
|
return 'stdin';
|
|
@@ -49,13 +49,6 @@ describe('presetPrompt', () => {
|
|
|
49
49
|
method: 'flag',
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
-
it('falls back to stdin for unknown commands', () => {
|
|
53
|
-
expect(preparePresetLaunch({ command: 'custom-agent', args: ['--interactive'] }, 'hello')).toEqual({
|
|
54
|
-
args: ['--interactive'],
|
|
55
|
-
method: 'stdin',
|
|
56
|
-
stdinPayload: 'hello\r',
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
52
|
describe('describePromptInjection', () => {
|
|
60
53
|
it('describes final-arg for claude', () => {
|
|
61
54
|
expect(describePromptInjection({
|
|
@@ -105,9 +98,6 @@ describe('presetPrompt', () => {
|
|
|
105
98
|
detectionStrategy: 'kimi',
|
|
106
99
|
})).toContain('-p');
|
|
107
100
|
});
|
|
108
|
-
it('describes stdin for unknown strategy', () => {
|
|
109
|
-
expect(describePromptInjection({ command: 'custom-agent' })).toContain('standard input');
|
|
110
|
-
});
|
|
111
101
|
});
|
|
112
102
|
describe('getPromptInjectionMethod', () => {
|
|
113
103
|
it('returns final-arg for claude', () => {
|
|
@@ -158,10 +148,8 @@ describe('presetPrompt', () => {
|
|
|
158
148
|
detectionStrategy: 'kimi',
|
|
159
149
|
})).toBe('flag');
|
|
160
150
|
});
|
|
161
|
-
it('
|
|
162
|
-
expect(getPromptInjectionMethod({ command: 'claude' })).toBe('
|
|
163
|
-
expect(getPromptInjectionMethod({ command: 'opencode' })).toBe('stdin');
|
|
164
|
-
expect(getPromptInjectionMethod({ command: 'custom-agent' })).toBe('stdin');
|
|
151
|
+
it('falls back to claude strategy when detectionStrategy is not set', () => {
|
|
152
|
+
expect(getPromptInjectionMethod({ command: 'claude' })).toBe('final-arg');
|
|
165
153
|
});
|
|
166
154
|
});
|
|
167
155
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.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.11.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "3.11.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "3.11.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "3.11.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "3.11.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "3.11.1",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.11.1",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.11.1",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.11.1",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.11.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|