ccmanager 3.0.0 → 3.1.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.
@@ -4,6 +4,7 @@ import SelectInput from 'ink-select-input';
4
4
  import { configurationManager } from '../services/configurationManager.js';
5
5
  import { shortcutManager } from '../services/shortcutManager.js';
6
6
  import ConfigureCustomCommand from './ConfigureCustomCommand.js';
7
+ import ConfigureTimeout from './ConfigureTimeout.js';
7
8
  import CustomCommandSummary from './CustomCommandSummary.js';
8
9
  const ConfigureOther = ({ onComplete }) => {
9
10
  const autoApprovalConfig = configurationManager.getAutoApprovalConfig();
@@ -11,6 +12,8 @@ const ConfigureOther = ({ onComplete }) => {
11
12
  const [autoApprovalEnabled, setAutoApprovalEnabled] = useState(autoApprovalConfig.enabled);
12
13
  const [customCommand, setCustomCommand] = useState(autoApprovalConfig.customCommand ?? '');
13
14
  const [customCommandDraft, setCustomCommandDraft] = useState(customCommand);
15
+ const [timeout, setTimeout] = useState(autoApprovalConfig.timeout ?? 30);
16
+ const [timeoutDraft, setTimeoutDraft] = useState(timeout);
14
17
  useInput((input, key) => {
15
18
  if (shortcutManager.matchesShortcut('cancel', input, key)) {
16
19
  if (view === 'customCommand') {
@@ -18,6 +21,11 @@ const ConfigureOther = ({ onComplete }) => {
18
21
  setView('main');
19
22
  return;
20
23
  }
24
+ if (view === 'timeout') {
25
+ setTimeoutDraft(timeout);
26
+ setView('main');
27
+ return;
28
+ }
21
29
  onComplete();
22
30
  }
23
31
  });
@@ -30,6 +38,10 @@ const ConfigureOther = ({ onComplete }) => {
30
38
  label: '✏️ Edit Custom Command',
31
39
  value: 'customCommand',
32
40
  },
41
+ {
42
+ label: `⏱️ Set Timeout (${timeout}s)`,
43
+ value: 'timeout',
44
+ },
33
45
  {
34
46
  label: '💾 Save Changes',
35
47
  value: 'save',
@@ -48,10 +60,15 @@ const ConfigureOther = ({ onComplete }) => {
48
60
  setCustomCommandDraft(customCommand);
49
61
  setView('customCommand');
50
62
  break;
63
+ case 'timeout':
64
+ setTimeoutDraft(timeout);
65
+ setView('timeout');
66
+ break;
51
67
  case 'save':
52
68
  configurationManager.setAutoApprovalConfig({
53
69
  enabled: autoApprovalEnabled,
54
70
  customCommand: customCommand.trim() || undefined,
71
+ timeout,
55
72
  });
56
73
  onComplete();
57
74
  break;
@@ -71,6 +88,15 @@ const ConfigureOther = ({ onComplete }) => {
71
88
  setView('main');
72
89
  } }));
73
90
  }
91
+ if (view === 'timeout') {
92
+ return (React.createElement(ConfigureTimeout, { value: timeoutDraft, onChange: setTimeoutDraft, onCancel: () => {
93
+ setTimeoutDraft(timeout);
94
+ setView('main');
95
+ }, onSubmit: value => {
96
+ setTimeout(value);
97
+ setView('main');
98
+ } }));
99
+ }
74
100
  return (React.createElement(Box, { flexDirection: "column" },
75
101
  React.createElement(Box, { marginBottom: 1 },
76
102
  React.createElement(Text, { bold: true, color: "green" }, "Other & Experimental Settings")),
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface ConfigureTimeoutProps {
3
+ value: number;
4
+ onChange: (value: number) => void;
5
+ onSubmit: (value: number) => void;
6
+ onCancel: () => void;
7
+ }
8
+ declare const ConfigureTimeout: React.FC<ConfigureTimeoutProps>;
9
+ export default ConfigureTimeout;
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import TextInputWrapper from './TextInputWrapper.js';
4
+ import { shortcutManager } from '../services/shortcutManager.js';
5
+ const ConfigureTimeout = ({ value, onChange, onSubmit, onCancel, }) => {
6
+ const [inputValue, setInputValue] = React.useState(String(value));
7
+ const handleChange = (newValue) => {
8
+ // Only allow numeric input
9
+ const filtered = newValue.replace(/[^0-9]/g, '');
10
+ setInputValue(filtered);
11
+ const parsed = parseInt(filtered, 10);
12
+ if (!isNaN(parsed) && parsed > 0) {
13
+ onChange(parsed);
14
+ }
15
+ };
16
+ const handleSubmit = () => {
17
+ const parsed = parseInt(inputValue, 10);
18
+ if (!isNaN(parsed) && parsed > 0) {
19
+ onSubmit(parsed);
20
+ }
21
+ };
22
+ useInput((input, key) => {
23
+ if (shortcutManager.matchesShortcut('cancel', input, key)) {
24
+ onCancel();
25
+ }
26
+ });
27
+ return (React.createElement(Box, { flexDirection: "column" },
28
+ React.createElement(Box, { marginBottom: 1 },
29
+ React.createElement(Text, { bold: true, color: "green" }, "Auto-Approval Timeout")),
30
+ React.createElement(Box, { marginBottom: 1 },
31
+ React.createElement(Text, null, "Enter timeout in seconds for auto-approval verification:")),
32
+ React.createElement(Box, { marginBottom: 1 },
33
+ React.createElement(TextInputWrapper, { value: inputValue, onChange: handleChange, onSubmit: handleSubmit, placeholder: "e.g. 30", focus: true })),
34
+ React.createElement(Box, { marginBottom: 1 },
35
+ React.createElement(Text, { dimColor: true }, "Must be a positive integer (minimum: 1 second, default: 30 seconds)")),
36
+ React.createElement(Box, null,
37
+ React.createElement(Text, { dimColor: true },
38
+ "Press Enter to save, ",
39
+ shortcutManager.getShortcutDisplay('cancel'),
40
+ " to go back"))));
41
+ };
42
+ export default ConfigureTimeout;
@@ -8,30 +8,6 @@ describe('ClaudeStateDetector', () => {
8
8
  detector = new ClaudeStateDetector();
9
9
  });
10
10
  describe('detectState', () => {
11
- it('should detect waiting_input when "Do you want" prompt is present', () => {
12
- // Arrange
13
- terminal = createMockTerminal([
14
- 'Some previous output',
15
- '│ Do you want to continue? (y/n)',
16
- '│ > ',
17
- ]);
18
- // Act
19
- const state = detector.detectState(terminal, 'idle');
20
- // Assert
21
- expect(state).toBe('waiting_input');
22
- });
23
- it('should detect waiting_input when "Would you like" prompt is present', () => {
24
- // Arrange
25
- terminal = createMockTerminal([
26
- 'Some output',
27
- '│ Would you like to save changes?',
28
- '│ > ',
29
- ]);
30
- // Act
31
- const state = detector.detectState(terminal, 'idle');
32
- // Assert
33
- expect(state).toBe('waiting_input');
34
- });
35
11
  it('should detect busy when "ESC to interrupt" is present', () => {
36
12
  // Arrange
37
13
  terminal = createMockTerminal([
@@ -74,37 +50,6 @@ describe('ClaudeStateDetector', () => {
74
50
  // Assert
75
51
  expect(state).toBe('idle');
76
52
  });
77
- it('should only consider last 30 lines', () => {
78
- // Arrange
79
- const lines = [];
80
- // Add more than 30 lines
81
- for (let i = 0; i < 40; i++) {
82
- lines.push(`Line ${i}`);
83
- }
84
- // The "Do you want" should be outside the 30 line window
85
- lines.push('│ Do you want to continue?');
86
- // Add 30 more lines to push it out
87
- for (let i = 0; i < 30; i++) {
88
- lines.push(`Recent line ${i}`);
89
- }
90
- terminal = createMockTerminal(lines);
91
- // Act
92
- const state = detector.detectState(terminal, 'idle');
93
- // Assert
94
- expect(state).toBe('idle'); // Should not detect the old prompt
95
- });
96
- it('should prioritize waiting_input over busy state', () => {
97
- // Arrange
98
- terminal = createMockTerminal([
99
- 'Press ESC to interrupt',
100
- '│ Do you want to continue?',
101
- '│ > ',
102
- ]);
103
- // Act
104
- const state = detector.detectState(terminal, 'idle');
105
- // Assert
106
- expect(state).toBe('waiting_input'); // waiting_input should take precedence
107
- });
108
53
  it('should maintain current state when "ctrl+r to toggle" is present', () => {
109
54
  // Arrange
110
55
  terminal = createMockTerminal([
@@ -222,91 +167,5 @@ describe('ClaudeStateDetector', () => {
222
167
  // Assert
223
168
  expect(state).toBe('waiting_input');
224
169
  });
225
- it('should detect waiting_input when "enter to select" is present', () => {
226
- // Arrange
227
- terminal = createMockTerminal([
228
- 'Select an option:',
229
- '',
230
- '❯ Option 1',
231
- ' Option 2',
232
- '',
233
- 'Enter to select',
234
- ]);
235
- // Act
236
- const state = detector.detectState(terminal, 'idle');
237
- // Assert
238
- expect(state).toBe('waiting_input');
239
- });
240
- it('should detect waiting_input when "tab/arrow keys to navigate" is present', () => {
241
- // Arrange
242
- terminal = createMockTerminal([
243
- 'Choose your action:',
244
- '',
245
- '❯ Continue',
246
- ' Skip',
247
- '',
248
- 'Tab/arrow keys to navigate',
249
- ]);
250
- // Act
251
- const state = detector.detectState(terminal, 'idle');
252
- // Assert
253
- expect(state).toBe('waiting_input');
254
- });
255
- it('should detect waiting_input when "esc to cancel" is present', () => {
256
- // Arrange
257
- terminal = createMockTerminal([
258
- 'Interactive selection:',
259
- '',
260
- '❯ Yes',
261
- ' No',
262
- '',
263
- 'Esc to cancel',
264
- ]);
265
- // Act
266
- const state = detector.detectState(terminal, 'idle');
267
- // Assert
268
- expect(state).toBe('waiting_input');
269
- });
270
- it('should detect waiting_input when "ready to submit your answers?" is present', () => {
271
- // Arrange
272
- terminal = createMockTerminal([
273
- 'Review your selections:',
274
- '',
275
- 'Choice 1: Yes',
276
- 'Choice 2: No',
277
- '',
278
- 'Ready to submit your answers?',
279
- ]);
280
- // Act
281
- const state = detector.detectState(terminal, 'idle');
282
- // Assert
283
- expect(state).toBe('waiting_input');
284
- });
285
- it('should detect waiting_input with mixed case interactive patterns', () => {
286
- // Arrange
287
- terminal = createMockTerminal([
288
- 'Select options:',
289
- '',
290
- 'ENTER TO SELECT',
291
- 'TAB/ARROW KEYS TO NAVIGATE',
292
- ]);
293
- // Act
294
- const state = detector.detectState(terminal, 'idle');
295
- // Assert
296
- expect(state).toBe('waiting_input');
297
- });
298
- it('should prioritize interactive patterns over busy state', () => {
299
- // Arrange
300
- terminal = createMockTerminal([
301
- 'Press ESC to interrupt',
302
- '',
303
- 'Select an option:',
304
- 'Enter to select',
305
- ]);
306
- // Act
307
- const state = detector.detectState(terminal, 'idle');
308
- // Assert
309
- expect(state).toBe('waiting_input'); // Interactive pattern should take precedence
310
- });
311
170
  });
312
171
  });
@@ -3,7 +3,12 @@ import { ProcessError } from '../types/errors.js';
3
3
  import { configurationManager } from './configurationManager.js';
4
4
  import { logger } from '../utils/logger.js';
5
5
  import { execFile, spawn, } from 'child_process';
6
- const AUTO_APPROVAL_TIMEOUT_MS = 60000;
6
+ const DEFAULT_TIMEOUT_SECONDS = 30;
7
+ const getTimeoutMs = () => {
8
+ const config = configurationManager.getAutoApprovalConfig();
9
+ const timeoutSeconds = config.timeout ?? DEFAULT_TIMEOUT_SECONDS;
10
+ return timeoutSeconds * 1000;
11
+ };
7
12
  const createAbortError = () => {
8
13
  const error = new Error('Auto-approval verification aborted');
9
14
  error.name = 'AbortError';
@@ -80,15 +85,16 @@ export class AutoApprovalVerifier {
80
85
  return;
81
86
  signal.removeEventListener('abort', abortListener);
82
87
  };
88
+ const timeoutMs = getTimeoutMs();
83
89
  const timeoutId = setTimeout(() => {
84
90
  settle(() => {
85
91
  logger.warn('Auto-approval verification timed out, terminating helper Claude process');
86
92
  if (child?.pid) {
87
93
  child.kill('SIGKILL');
88
94
  }
89
- reject(new Error('Auto-approval verification timed out after 60s'));
95
+ reject(new Error(`Auto-approval verification timed out after ${timeoutMs / 1000}s`));
90
96
  });
91
- }, AUTO_APPROVAL_TIMEOUT_MS);
97
+ }, timeoutMs);
92
98
  if (signal) {
93
99
  if (signal.aborted) {
94
100
  abortListener();
@@ -166,13 +172,14 @@ export class AutoApprovalVerifier {
166
172
  const child = spawn(command, [], spawnOptions);
167
173
  let stdout = '';
168
174
  let stderr = '';
175
+ const timeoutMs = getTimeoutMs();
169
176
  timeoutId = setTimeout(() => {
170
177
  logger.warn('Auto-approval custom command timed out, terminating process');
171
178
  settle(() => {
172
179
  child.kill('SIGKILL');
173
- reject(new Error('Auto-approval verification custom command timed out after 60s'));
180
+ reject(new Error(`Auto-approval verification custom command timed out after ${timeoutMs / 1000}s`));
174
181
  });
175
- }, AUTO_APPROVAL_TIMEOUT_MS);
182
+ }, timeoutMs);
176
183
  if (signal) {
177
184
  if (signal.aborted) {
178
185
  abortListener();
@@ -24,6 +24,7 @@ export declare class ConfigurationManager {
24
24
  getAutoApprovalConfig(): NonNullable<ConfigurationData['autoApproval']>;
25
25
  setAutoApprovalConfig(autoApproval: NonNullable<ConfigurationData['autoApproval']>): void;
26
26
  setAutoApprovalEnabled(enabled: boolean): void;
27
+ setAutoApprovalTimeout(timeout: number): void;
27
28
  getCommandConfig(): CommandConfig;
28
29
  setCommandConfig(commandConfig: CommandConfig): void;
29
30
  private migrateLegacyCommandToPresets;
@@ -102,10 +102,16 @@ export class ConfigurationManager {
102
102
  if (!this.config.autoApproval) {
103
103
  this.config.autoApproval = {
104
104
  enabled: false,
105
+ timeout: 30,
105
106
  };
106
107
  }
107
- else if (!Object.prototype.hasOwnProperty.call(this.config.autoApproval, 'enabled')) {
108
- this.config.autoApproval.enabled = false;
108
+ else {
109
+ if (!Object.prototype.hasOwnProperty.call(this.config.autoApproval, 'enabled')) {
110
+ this.config.autoApproval.enabled = false;
111
+ }
112
+ if (!Object.prototype.hasOwnProperty.call(this.config.autoApproval, 'timeout')) {
113
+ this.config.autoApproval.timeout = 30;
114
+ }
109
115
  }
110
116
  // Migrate legacy command config to presets if needed
111
117
  this.migrateLegacyCommandToPresets();
@@ -174,9 +180,14 @@ export class ConfigurationManager {
174
180
  this.saveConfig();
175
181
  }
176
182
  getAutoApprovalConfig() {
177
- return (this.config.autoApproval || {
183
+ const config = this.config.autoApproval || {
178
184
  enabled: false,
179
- });
185
+ };
186
+ // Default timeout to 30 seconds if not set
187
+ return {
188
+ ...config,
189
+ timeout: config.timeout ?? 30,
190
+ };
180
191
  }
181
192
  setAutoApprovalConfig(autoApproval) {
182
193
  this.config.autoApproval = autoApproval;
@@ -186,6 +197,10 @@ export class ConfigurationManager {
186
197
  const currentConfig = this.getAutoApprovalConfig();
187
198
  this.setAutoApprovalConfig({ ...currentConfig, enabled });
188
199
  }
200
+ setAutoApprovalTimeout(timeout) {
201
+ const currentConfig = this.getAutoApprovalConfig();
202
+ this.setAutoApprovalConfig({ ...currentConfig, timeout });
203
+ }
189
204
  getCommandConfig() {
190
205
  // For backward compatibility, return the default preset as CommandConfig
191
206
  const defaultPreset = this.getDefaultPreset();
@@ -535,10 +550,16 @@ export class ConfigurationManager {
535
550
  if (!config.autoApproval) {
536
551
  config.autoApproval = {
537
552
  enabled: false,
553
+ timeout: 30,
538
554
  };
539
555
  }
540
- else if (!Object.prototype.hasOwnProperty.call(config.autoApproval, 'enabled')) {
541
- config.autoApproval.enabled = false;
556
+ else {
557
+ if (!Object.prototype.hasOwnProperty.call(config.autoApproval, 'enabled')) {
558
+ config.autoApproval.enabled = false;
559
+ }
560
+ if (!Object.prototype.hasOwnProperty.call(config.autoApproval, 'timeout')) {
561
+ config.autoApproval.timeout = 30;
562
+ }
542
563
  }
543
564
  return config;
544
565
  }
@@ -127,7 +127,7 @@ describe('SessionManager - State Persistence', () => {
127
127
  vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS * 2);
128
128
  expect(session.pendingState).toBe('idle');
129
129
  // Simulate output that would trigger waiting_input state
130
- eventEmitter.emit('data', 'Do you want to continue?');
130
+ eventEmitter.emit('data', 'Do you want to continue?\n❯ 1. Yes');
131
131
  // Advance time to trigger another check
132
132
  vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS);
133
133
  // Pending state should now be waiting_input, not idle
@@ -172,7 +172,7 @@ describe('SessionManager - State Persistence', () => {
172
172
  expect(session.pendingState).toBe('idle');
173
173
  // Now change to a different state before idle persists
174
174
  // Clear terminal first and add waiting prompt
175
- eventEmitter.emit('data', '\x1b[2J\x1b[H│ Do you want to continue?\n');
175
+ eventEmitter.emit('data', '\x1b[2J\x1b[HDo you want to continue?\n❯ 1. Yes');
176
176
  // Advance time to detect new state but still less than persistence duration from first change
177
177
  vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS); // Another 100ms, total 200ms exactly at threshold
178
178
  // Pending state should have changed to waiting_input
@@ -210,7 +210,7 @@ describe('SessionManager - State Persistence', () => {
210
210
  // Session 1 goes to idle
211
211
  eventEmitter1.emit('data', 'Idle output for session 1');
212
212
  // Session 2 goes to waiting_input
213
- eventEmitter2.emit('data', 'Do you want to continue?');
213
+ eventEmitter2.emit('data', 'Do you want to continue?\n❯ 1. Yes');
214
214
  // Advance time to check but not confirm
215
215
  vi.advanceTimersByTime(STATE_CHECK_INTERVAL_MS * 2);
216
216
  // Both should have pending states but not changed yet
@@ -45,20 +45,6 @@ export class ClaudeStateDetector extends BaseStateDetector {
45
45
  if (lowerContent.includes('ctrl+r to toggle')) {
46
46
  return currentState;
47
47
  }
48
- // Check for interactive selection interface patterns
49
- // These patterns indicate Claude is waiting for user interaction with navigation/selection UI
50
- const hasInteractivePattern = lowerContent.includes('enter to select') ||
51
- lowerContent.includes('tab/arrow keys to navigate') ||
52
- lowerContent.includes('esc to cancel') ||
53
- lowerContent.includes('ready to submit your answers?');
54
- if (hasInteractivePattern) {
55
- return 'waiting_input';
56
- }
57
- // Check for waiting prompts with box character
58
- if (content.includes('│ Do you want') ||
59
- content.includes('│ Would you like')) {
60
- return 'waiting_input';
61
- }
62
48
  // Check for "Do you want" or "Would you like" pattern with options
63
49
  // Handles both simple ("Do you want...\nYes") and complex (numbered options) formats
64
50
  if (/(?:do you want|would you like).+\n+[\s\S]*?(?:yes|❯)/.test(lowerContent)) {
@@ -110,6 +110,7 @@ export interface ConfigurationData {
110
110
  autoApproval?: {
111
111
  enabled: boolean;
112
112
  customCommand?: string;
113
+ timeout?: number;
113
114
  };
114
115
  }
115
116
  export interface GitProject {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",