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.
@@ -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
- }, [sessionManager, navigateWithClear, navigateToSession]);
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 TextInput from 'ink-text-input';
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 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);
12
- };
13
- return _jsx(TextInput, { value: value, onChange: handleChange, ...props });
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', ['-p', '--output-format', 'json', '--json-schema', JSON_SCHEMA], {
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
- if (preset.detectionStrategy && PROMPT_FLAG[preset.detectionStrategy]) {
18
+ const strategy = preset.detectionStrategy ?? DEFAULT_DETECTION_STRATEGY;
19
+ if (PROMPT_FLAG[strategy]) {
18
20
  return 'flag';
19
21
  }
20
- if (preset.detectionStrategy === 'claude' ||
21
- preset.detectionStrategy === 'codex' ||
22
- preset.detectionStrategy === 'cursor' ||
23
- preset.detectionStrategy === 'cline') {
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('returns stdin when detectionStrategy is not set', () => {
162
- expect(getPromptInjectionMethod({ command: 'claude' })).toBe('stdin');
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.0",
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.0",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "3.11.0",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "3.11.0",
47
- "@kodaikabasawa/ccmanager-linux-x64": "3.11.0",
48
- "@kodaikabasawa/ccmanager-win32-x64": "3.11.0"
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",