ccmanager 2.2.1 → 2.2.3

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 CHANGED
@@ -1,13 +1,11 @@
1
- # CCManager - AI Code Assistant Session Manager
2
-
3
- CCManager is a TUI application for managing multiple AI coding assistant sessions (Claude Code, Gemini CLI) across Git worktrees and projects.
1
+ # CCManager - AI Code Agent Session Manager
4
2
 
3
+ [![Mentioned in Awesome Gemini CLI](https://awesome.re/mentioned-badge.svg)](https://github.com/Piebald-AI/awesome-gemini-cli)
5
4
 
5
+ CCManager is a CLI application for managing multiple AI coding assistant sessions (Claude Code, Gemini CLI, Codex CLI) across Git worktrees and projects.
6
6
 
7
7
  https://github.com/user-attachments/assets/15914a88-e288-4ac9-94d5-8127f2e19dbf
8
8
 
9
-
10
-
11
9
  ## Features
12
10
 
13
11
  - Run multiple AI assistant sessions in parallel across different Git worktrees
@@ -4,6 +4,7 @@ import TextInputWrapper from './TextInputWrapper.js';
4
4
  import SelectInput from 'ink-select-input';
5
5
  import { configurationManager } from '../services/configurationManager.js';
6
6
  import { shortcutManager } from '../services/shortcutManager.js';
7
+ import Confirmation from './Confirmation.js';
7
8
  const formatDetectionStrategy = (strategy) => {
8
9
  const value = strategy || 'claude';
9
10
  switch (value) {
@@ -354,12 +355,8 @@ const ConfigureCommand = ({ onComplete }) => {
354
355
  // Render delete confirmation
355
356
  if (viewMode === 'delete-confirm') {
356
357
  const preset = presets.find(p => p.id === selectedPresetId);
357
- const confirmItems = [
358
- { label: 'Yes, delete', value: 'yes' },
359
- { label: 'Cancel', value: 'cancel' },
360
- ];
361
- const handleConfirmSelect = (item) => {
362
- if (item.value === 'yes') {
358
+ const handleConfirmSelect = (value) => {
359
+ if (value === 'yes') {
363
360
  handleDeleteConfirm();
364
361
  }
365
362
  else {
@@ -367,23 +364,16 @@ const ConfigureCommand = ({ onComplete }) => {
367
364
  setSelectedIndex(6); // Return to delete option in edit menu
368
365
  }
369
366
  };
370
- return (React.createElement(Box, { flexDirection: "column" },
371
- React.createElement(Box, { marginBottom: 1 },
372
- React.createElement(Text, { bold: true, color: "red" }, "Confirm Delete")),
373
- React.createElement(Box, { marginBottom: 1 },
374
- React.createElement(Text, null,
375
- "Delete preset \"",
376
- preset?.name,
377
- "\"?")),
378
- React.createElement(SelectInput, { items: confirmItems, onSelect: handleConfirmSelect, initialIndex: 1, indicatorComponent: ({ isSelected }) => (React.createElement(Text, { color: isSelected ? 'red' : undefined }, isSelected ? '>' : ' ')), itemComponent: ({ isSelected, label }) => (React.createElement(Text, { color: label === 'Yes, delete'
379
- ? isSelected
380
- ? 'red'
381
- : undefined
382
- : isSelected
383
- ? 'cyan'
384
- : undefined, inverse: isSelected }, label)) }),
385
- React.createElement(Box, { marginTop: 1 },
386
- React.createElement(Text, { dimColor: true }, "Press \u2191\u2193/j/k to navigate, Enter to confirm"))));
367
+ const title = (React.createElement(Text, { bold: true, color: "red" }, "Confirm Delete"));
368
+ const message = React.createElement(Text, null,
369
+ "Delete preset \"",
370
+ preset?.name,
371
+ "\"?");
372
+ const hint = (React.createElement(Text, { dimColor: true }, "Press \u2191\u2193/j/k to navigate, Enter to confirm"));
373
+ return (React.createElement(Confirmation, { title: title, message: message, options: [
374
+ { label: 'Yes, delete', value: 'yes', color: 'red' },
375
+ { label: 'Cancel', value: 'cancel', color: 'cyan' },
376
+ ], onSelect: handleConfirmSelect, initialIndex: 1, indicatorColor: "red", hint: hint }));
387
377
  }
388
378
  // Render edit preset view
389
379
  if (viewMode === 'edit') {
@@ -1,5 +1,29 @@
1
1
  import React from 'react';
2
+ export interface ConfirmationOption {
3
+ label: string;
4
+ value: string;
5
+ color?: string;
6
+ }
2
7
  interface ConfirmationProps {
8
+ title?: React.ReactNode;
9
+ message?: React.ReactNode;
10
+ options: ConfirmationOption[];
11
+ onSelect: (value: string) => void;
12
+ initialIndex?: number;
13
+ indicatorColor?: string;
14
+ hint?: React.ReactNode;
15
+ onCancel?: () => void;
16
+ onEscape?: () => void;
17
+ onCustomInput?: (input: string, key: {
18
+ [key: string]: boolean;
19
+ }) => boolean;
20
+ }
21
+ /**
22
+ * Reusable confirmation component with SelectInput UI pattern
23
+ */
24
+ declare const Confirmation: React.FC<ConfirmationProps>;
25
+ export default Confirmation;
26
+ interface SimpleConfirmationProps {
3
27
  message: string | React.ReactNode;
4
28
  onConfirm: () => void;
5
29
  onCancel: () => void;
@@ -8,5 +32,4 @@ interface ConfirmationProps {
8
32
  confirmColor?: string;
9
33
  cancelColor?: string;
10
34
  }
11
- declare const Confirmation: React.FC<ConfirmationProps>;
12
- export default Confirmation;
35
+ export declare const SimpleConfirmation: React.FC<SimpleConfirmationProps>;
@@ -1,42 +1,63 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
+ import SelectInput from 'ink-select-input';
3
4
  import { shortcutManager } from '../services/shortcutManager.js';
4
- const Confirmation = ({ message, onConfirm, onCancel, confirmText = 'Yes', cancelText = 'No', confirmColor = 'green', cancelColor = 'red', }) => {
5
- const [focused, setFocused] = useState(true); // true = confirm, false = cancel
5
+ /**
6
+ * Reusable confirmation component with SelectInput UI pattern
7
+ */
8
+ const Confirmation = ({ title, message, options, onSelect, initialIndex = 0, indicatorColor, hint, onCancel, onEscape, onCustomInput, }) => {
6
9
  useInput((input, key) => {
7
- if (key.leftArrow || key.rightArrow) {
8
- setFocused(!focused);
10
+ // Check custom input handler first
11
+ if (onCustomInput && onCustomInput(input, key)) {
12
+ return;
9
13
  }
10
- else if (key.return) {
11
- if (focused) {
12
- onConfirm();
13
- }
14
- else {
15
- onCancel();
16
- }
17
- }
18
- else if (shortcutManager.matchesShortcut('cancel', input, key)) {
14
+ // Handle cancel shortcut
15
+ if (onCancel && shortcutManager.matchesShortcut('cancel', input, key)) {
19
16
  onCancel();
17
+ return;
18
+ }
19
+ // Handle escape key
20
+ if (onEscape && key['escape']) {
21
+ onEscape();
22
+ return;
20
23
  }
21
24
  });
25
+ const handleSelect = (item) => {
26
+ onSelect(item.value);
27
+ };
22
28
  return (React.createElement(Box, { flexDirection: "column" },
23
- React.createElement(Box, { marginBottom: 1 }, message),
24
- React.createElement(Box, null,
25
- React.createElement(Box, { marginRight: 2 },
26
- React.createElement(Text, { color: focused ? confirmColor : 'white', inverse: focused },
27
- ' ',
28
- confirmText,
29
- ' ')),
30
- React.createElement(Box, null,
31
- React.createElement(Text, { color: !focused ? cancelColor : 'white', inverse: !focused },
32
- ' ',
33
- cancelText,
34
- ' '))),
29
+ title && React.createElement(Box, { marginBottom: 1 }, title),
30
+ message && React.createElement(Box, { marginBottom: 1 }, message),
35
31
  React.createElement(Box, { marginTop: 1 },
36
- React.createElement(Text, { dimColor: true },
37
- "Use \u2190 \u2192 to navigate, Enter to select,",
38
- ' ',
39
- shortcutManager.getShortcutDisplay('cancel'),
40
- " to cancel"))));
32
+ React.createElement(SelectInput, { items: options, onSelect: handleSelect, initialIndex: initialIndex, indicatorComponent: ({ isSelected }) => (React.createElement(Text, { color: isSelected && indicatorColor ? indicatorColor : undefined }, isSelected ? '>' : ' ')), itemComponent: ({ isSelected, label }) => {
33
+ // Find the color for this option
34
+ const option = options.find(opt => opt.label === label);
35
+ const color = option?.color;
36
+ return (React.createElement(Text, { color: isSelected && color ? color : isSelected ? undefined : 'white', inverse: isSelected },
37
+ ' ',
38
+ label,
39
+ ' '));
40
+ } })),
41
+ hint && React.createElement(Box, { marginTop: 1 }, hint)));
41
42
  };
42
43
  export default Confirmation;
44
+ export const SimpleConfirmation = ({ message, onConfirm, onCancel, confirmText = 'Yes', cancelText = 'No', confirmColor = 'green', cancelColor = 'red', }) => {
45
+ const options = [
46
+ { label: confirmText, value: 'confirm', color: confirmColor },
47
+ { label: cancelText, value: 'cancel', color: cancelColor },
48
+ ];
49
+ const handleSelect = (value) => {
50
+ if (value === 'confirm') {
51
+ onConfirm();
52
+ }
53
+ else {
54
+ onCancel();
55
+ }
56
+ };
57
+ const hint = (React.createElement(Text, { dimColor: true },
58
+ "Use \u2191\u2193/j/k to navigate, Enter to select,",
59
+ ' ',
60
+ shortcutManager.getShortcutDisplay('cancel'),
61
+ " to cancel"));
62
+ return (React.createElement(Confirmation, { message: message, options: options, onSelect: handleSelect, initialIndex: 0, hint: hint, onCancel: onCancel }));
63
+ };
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
4
  import { shortcutManager } from '../services/shortcutManager.js';
5
+ import Confirmation from './Confirmation.js';
5
6
  const DeleteConfirmation = ({ worktrees, onConfirm, onCancel, }) => {
6
7
  // Check if any worktrees have branches
7
8
  const hasAnyBranches = worktrees.some(wt => wt.branch);
@@ -19,36 +20,26 @@ const DeleteConfirmation = ({ worktrees, onConfirm, onCancel, }) => {
19
20
  value: 'keepBranch',
20
21
  },
21
22
  ];
22
- // Menu items for actions
23
- const actionOptions = [
24
- { label: 'Confirm', value: 'confirm' },
25
- { label: 'Cancel', value: 'cancel' },
26
- ];
27
23
  const handleBranchSelect = (item) => {
28
24
  // Don't toggle on Enter - only update focused option
29
25
  setFocusedOption(item.value);
30
26
  };
31
- const handleActionSelect = (item) => {
32
- if (item.value === 'confirm') {
27
+ const handleActionSelect = (value) => {
28
+ if (value === 'confirm') {
33
29
  onConfirm(deleteBranch);
34
30
  }
35
31
  else {
36
32
  onCancel();
37
33
  }
38
34
  };
39
- useInput((input, key) => {
40
- if (shortcutManager.matchesShortcut('cancel', input, key)) {
41
- onCancel();
42
- }
43
- else if (hasAnyBranches && view === 'options' && key.return) {
44
- // Move to confirm view when Enter is pressed in options
35
+ // Handle keyboard input for branch options view
36
+ const handleBranchOptionsInput = (input, key) => {
37
+ if (key['return']) {
38
+ // Move to confirm view when Enter is pressed
45
39
  setView('confirm');
40
+ return true;
46
41
  }
47
- else if (hasAnyBranches && view === 'confirm' && key.escape) {
48
- // Go back to options when Escape is pressed in confirm
49
- setView('options');
50
- }
51
- else if (hasAnyBranches && view === 'options' && input === ' ') {
42
+ else if (input === ' ') {
52
43
  // Toggle selection on space for radio buttons
53
44
  if (focusedOption === 'deleteBranch') {
54
45
  setDeleteBranch(true);
@@ -56,63 +47,75 @@ const DeleteConfirmation = ({ worktrees, onConfirm, onCancel, }) => {
56
47
  else {
57
48
  setDeleteBranch(false);
58
49
  }
50
+ return true;
51
+ }
52
+ return false;
53
+ };
54
+ useInput((input, key) => {
55
+ if (hasAnyBranches && view === 'options') {
56
+ if (handleBranchOptionsInput(input, key)) {
57
+ return;
58
+ }
59
+ if (shortcutManager.matchesShortcut('cancel', input, key)) {
60
+ onCancel();
61
+ }
59
62
  }
60
63
  });
61
- return (React.createElement(Box, { flexDirection: "column" },
62
- React.createElement(Text, { bold: true, color: "red" }, "\u26A0\uFE0F Delete Confirmation"),
63
- React.createElement(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column" },
64
- React.createElement(Text, null, "You are about to delete the following worktrees:"),
65
- worktrees.length <= 10 ? (worktrees.map(wt => (React.createElement(Text, { key: wt.path, color: "red" },
64
+ // Title component
65
+ const title = (React.createElement(Text, { bold: true, color: "red" }, "\u26A0\uFE0F Delete Confirmation"));
66
+ // Message component
67
+ const message = (React.createElement(Box, { flexDirection: "column" },
68
+ React.createElement(Text, null, "You are about to delete the following worktrees:"),
69
+ worktrees.length <= 10 ? (worktrees.map(wt => (React.createElement(Text, { key: wt.path, color: "red" },
70
+ "\u2022 ",
71
+ wt.branch ? wt.branch.replace('refs/heads/', '') : 'detached',
72
+ " (",
73
+ wt.path,
74
+ ")")))) : (React.createElement(React.Fragment, null,
75
+ worktrees.slice(0, 8).map(wt => (React.createElement(Text, { key: wt.path, color: "red" },
66
76
  "\u2022 ",
67
77
  wt.branch ? wt.branch.replace('refs/heads/', '') : 'detached',
68
78
  ' ',
69
79
  "(",
70
80
  wt.path,
71
- ")")))) : (React.createElement(React.Fragment, null,
72
- worktrees.slice(0, 8).map(wt => (React.createElement(Text, { key: wt.path, color: "red" },
73
- "\u2022",
74
- ' ',
75
- wt.branch ? wt.branch.replace('refs/heads/', '') : 'detached',
76
- ' ',
77
- "(",
78
- wt.path,
79
- ")"))),
80
- React.createElement(Text, { color: "red", dimColor: true },
81
- "... and ",
82
- worktrees.length - 8,
83
- " more worktrees")))),
84
- hasAnyBranches && view === 'options' && (React.createElement(Box, { marginBottom: 1, flexDirection: "column" },
85
- React.createElement(Text, { bold: true }, "What do you want to do with the associated branches?"),
81
+ ")"))),
82
+ React.createElement(Text, { color: "red", dimColor: true },
83
+ "... and ",
84
+ worktrees.length - 8,
85
+ " more worktrees")))));
86
+ if (hasAnyBranches && view === 'options') {
87
+ return (React.createElement(Box, { flexDirection: "column" },
88
+ title,
89
+ React.createElement(Box, { marginTop: 1, marginBottom: 1 }, message),
90
+ React.createElement(Box, { marginBottom: 1, flexDirection: "column" },
91
+ React.createElement(Text, { bold: true }, "What do you want to do with the associated branches?"),
92
+ React.createElement(Box, { marginTop: 1 },
93
+ React.createElement(SelectInput, { items: branchOptions, onSelect: handleBranchSelect, onHighlight: (item) => {
94
+ setFocusedOption(item.value);
95
+ }, initialIndex: deleteBranch ? 0 : 1, indicatorComponent: ({ isSelected }) => (React.createElement(Text, { color: isSelected ? 'red' : undefined }, isSelected ? '>' : ' ')), itemComponent: ({ isSelected, label }) => (React.createElement(Text, { color: isSelected ? 'red' : undefined, inverse: isSelected }, label)) }))),
86
96
  React.createElement(Box, { marginTop: 1 },
87
- React.createElement(SelectInput, { items: branchOptions, onSelect: handleBranchSelect, onHighlight: (item) => {
88
- setFocusedOption(item.value);
89
- }, initialIndex: deleteBranch ? 0 : 1, indicatorComponent: ({ isSelected }) => (React.createElement(Text, { color: isSelected ? 'red' : undefined }, isSelected ? '>' : ' ')), itemComponent: ({ isSelected, label }) => (React.createElement(Text, { color: isSelected ? 'red' : undefined, inverse: isSelected }, label)) })))),
90
- hasAnyBranches && view === 'confirm' && (React.createElement(Box, { marginBottom: 1, flexDirection: "column" },
97
+ React.createElement(Text, { dimColor: true },
98
+ "Use \u2191\u2193/j/k to navigate, Space to toggle, Enter to continue,",
99
+ ' ',
100
+ shortcutManager.getShortcutDisplay('cancel'),
101
+ " to cancel"))));
102
+ }
103
+ // Confirmation view (either after selecting branch option or if no branches)
104
+ const confirmHint = (React.createElement(Text, { dimColor: true },
105
+ "Use \u2191\u2193/j/k to navigate, Enter to select,",
106
+ ' ',
107
+ shortcutManager.getShortcutDisplay('cancel'),
108
+ " to cancel"));
109
+ const confirmMessage = (React.createElement(Box, { flexDirection: "column" },
110
+ message,
111
+ hasAnyBranches && view === 'confirm' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
91
112
  React.createElement(Text, { bold: true }, "Branch option selected:"),
92
- React.createElement(Text, { color: "yellow" }, deleteBranch ? '✓ Delete the branches too' : '✓ Keep the branches'))),
93
- (view === 'confirm' || !hasAnyBranches) && (React.createElement(Box, { marginTop: 1 },
94
- React.createElement(SelectInput, { items: actionOptions, onSelect: handleActionSelect, initialIndex: 1, indicatorComponent: ({ isSelected }) => (React.createElement(Text, null, isSelected ? '>' : ' ')), itemComponent: ({ isSelected, label }) => {
95
- const color = label === 'Confirm' ? 'green' : 'red';
96
- return (React.createElement(Text, { color: isSelected ? color : 'white', inverse: isSelected },
97
- ' ',
98
- label,
99
- ' '));
100
- } }))),
101
- React.createElement(Box, { marginTop: 1 },
102
- React.createElement(Text, { dimColor: true }, hasAnyBranches && view === 'options' ? (React.createElement(React.Fragment, null,
103
- "Use \u2191\u2193/j/k to navigate, Space to toggle, Enter to continue,",
104
- ' ',
105
- shortcutManager.getShortcutDisplay('cancel'),
106
- " to cancel")) : view === 'confirm' ? (React.createElement(React.Fragment, null,
107
- "Use \u2191\u2193/j/k to navigate, Enter to select",
108
- hasAnyBranches ? ', Esc to go back' : '',
109
- ",",
110
- ' ',
111
- shortcutManager.getShortcutDisplay('cancel'),
112
- " to cancel")) : (React.createElement(React.Fragment, null,
113
- "Use \u2191\u2193/j/k to navigate, Enter to select,",
114
- ' ',
115
- shortcutManager.getShortcutDisplay('cancel'),
116
- " to cancel"))))));
113
+ React.createElement(Text, { color: "yellow" }, deleteBranch ? '✓ Delete the branches too' : '✓ Keep the branches')))));
114
+ return (React.createElement(Confirmation, { title: title, message: confirmMessage, options: [
115
+ { label: 'Confirm', value: 'confirm', color: 'green' },
116
+ { label: 'Cancel', value: 'cancel', color: 'red' },
117
+ ], onSelect: handleActionSelect, initialIndex: 1, hint: confirmHint, onCancel: onCancel, onEscape: hasAnyBranches && view === 'confirm'
118
+ ? () => setView('options')
119
+ : undefined }));
117
120
  };
118
121
  export default DeleteConfirmation;
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
4
  import { WorktreeService } from '../services/worktreeService.js';
5
- import Confirmation from './Confirmation.js';
5
+ import Confirmation, { SimpleConfirmation } from './Confirmation.js';
6
6
  import { shortcutManager } from '../services/shortcutManager.js';
7
7
  const MergeWorktree = ({ onComplete, onCancel, }) => {
8
8
  const [step, setStep] = useState('select-source');
@@ -11,7 +11,6 @@ const MergeWorktree = ({ onComplete, onCancel, }) => {
11
11
  const [branchItems, setBranchItems] = useState([]);
12
12
  const [originalBranchItems, setOriginalBranchItems] = useState([]);
13
13
  const [useRebase, setUseRebase] = useState(false);
14
- const [operationFocused, setOperationFocused] = useState(false);
15
14
  const [mergeError, setMergeError] = useState(null);
16
15
  const [worktreeService] = useState(() => new WorktreeService());
17
16
  useEffect(() => {
@@ -30,16 +29,7 @@ const MergeWorktree = ({ onComplete, onCancel, }) => {
30
29
  onCancel();
31
30
  return;
32
31
  }
33
- if (step === 'select-operation') {
34
- if (key.leftArrow || key.rightArrow) {
35
- const newOperationFocused = !operationFocused;
36
- setOperationFocused(newOperationFocused);
37
- setUseRebase(newOperationFocused);
38
- }
39
- else if (key.return) {
40
- setStep('confirm-merge');
41
- }
42
- }
32
+ // Operation selection is now handled by ConfirmationView
43
33
  if (step === 'merge-error') {
44
34
  // Any key press returns to menu
45
35
  onCancel();
@@ -105,34 +95,27 @@ const MergeWorktree = ({ onComplete, onCancel, }) => {
105
95
  " to cancel"))));
106
96
  }
107
97
  if (step === 'select-operation') {
108
- return (React.createElement(Box, { flexDirection: "column" },
109
- React.createElement(Box, { marginBottom: 1 },
110
- React.createElement(Text, { bold: true, color: "green" }, "Select Operation")),
111
- React.createElement(Box, { marginBottom: 1 },
112
- React.createElement(Text, null,
113
- "Choose how to integrate ",
114
- React.createElement(Text, { color: "yellow" }, sourceBranch),
115
- ' ',
116
- "into ",
117
- React.createElement(Text, { color: "yellow" }, targetBranch),
118
- ":")),
119
- React.createElement(Box, null,
120
- React.createElement(Box, { marginRight: 2 },
121
- React.createElement(Text, { color: !operationFocused ? 'green' : 'white', inverse: !operationFocused },
122
- ' ',
123
- "Merge",
124
- ' ')),
125
- React.createElement(Box, null,
126
- React.createElement(Text, { color: operationFocused ? 'blue' : 'white', inverse: operationFocused },
127
- ' ',
128
- "Rebase",
129
- ' '))),
130
- React.createElement(Box, { marginTop: 1 },
131
- React.createElement(Text, { dimColor: true },
132
- "Use \u2190 \u2192 to navigate, Enter to select,",
133
- ' ',
134
- shortcutManager.getShortcutDisplay('cancel'),
135
- " to cancel"))));
98
+ const title = (React.createElement(Text, { bold: true, color: "green" }, "Select Operation"));
99
+ const message = (React.createElement(Text, null,
100
+ "Choose how to integrate ",
101
+ React.createElement(Text, { color: "yellow" }, sourceBranch),
102
+ " into",
103
+ ' ',
104
+ React.createElement(Text, { color: "yellow" }, targetBranch),
105
+ ":"));
106
+ const hint = (React.createElement(Text, { dimColor: true },
107
+ "Use \u2191\u2193/j/k to navigate, Enter to select,",
108
+ ' ',
109
+ shortcutManager.getShortcutDisplay('cancel'),
110
+ " to cancel"));
111
+ const handleOperationSelect = (value) => {
112
+ setUseRebase(value === 'rebase');
113
+ setStep('confirm-merge');
114
+ };
115
+ return (React.createElement(Confirmation, { title: title, message: message, options: [
116
+ { label: 'Merge', value: 'merge', color: 'green' },
117
+ { label: 'Rebase', value: 'rebase', color: 'blue' },
118
+ ], onSelect: handleOperationSelect, initialIndex: 0, hint: hint }));
136
119
  }
137
120
  if (step === 'confirm-merge') {
138
121
  const confirmMessage = (React.createElement(Box, { flexDirection: "column" },
@@ -149,7 +132,7 @@ const MergeWorktree = ({ onComplete, onCancel, }) => {
149
132
  ' ',
150
133
  React.createElement(Text, { color: "yellow" }, targetBranch),
151
134
  "?")));
152
- return (React.createElement(Confirmation, { message: confirmMessage, onConfirm: () => setStep('executing-merge'), onCancel: onCancel }));
135
+ return (React.createElement(SimpleConfirmation, { message: confirmMessage, onConfirm: () => setStep('executing-merge'), onCancel: onCancel }));
153
136
  }
154
137
  if (step === 'executing-merge') {
155
138
  return (React.createElement(Box, { flexDirection: "column" },
@@ -177,7 +160,7 @@ const MergeWorktree = ({ onComplete, onCancel, }) => {
177
160
  React.createElement(Text, { color: "yellow" }, sourceBranch),
178
161
  ' ',
179
162
  "and its worktree?")));
180
- return (React.createElement(Confirmation, { message: deleteMessage, onConfirm: () => {
163
+ return (React.createElement(SimpleConfirmation, { message: deleteMessage, onConfirm: () => {
181
164
  const deleteResult = worktreeService.deleteWorktreeByBranch(sourceBranch);
182
165
  if (deleteResult.success) {
183
166
  onComplete();
@@ -24,7 +24,7 @@ export class SessionManager extends EventEmitter {
24
24
  // Create a detector based on the session's detection strategy
25
25
  const strategy = session.detectionStrategy || 'claude';
26
26
  const detector = createStateDetector(strategy);
27
- return detector.detectState(session.terminal);
27
+ return detector.detectState(session.terminal, session.state);
28
28
  }
29
29
  constructor() {
30
30
  super();
@@ -1,19 +1,19 @@
1
1
  import { SessionState, Terminal, StateDetectionStrategy } from '../types/index.js';
2
2
  export interface StateDetector {
3
- detectState(terminal: Terminal): SessionState;
3
+ detectState(terminal: Terminal, currentState: SessionState): SessionState;
4
4
  }
5
5
  export declare function createStateDetector(strategy?: StateDetectionStrategy): StateDetector;
6
6
  export declare abstract class BaseStateDetector implements StateDetector {
7
- abstract detectState(terminal: Terminal): SessionState;
7
+ abstract detectState(terminal: Terminal, currentState: SessionState): SessionState;
8
8
  protected getTerminalLines(terminal: Terminal, maxLines?: number): string[];
9
9
  protected getTerminalContent(terminal: Terminal, maxLines?: number): string;
10
10
  }
11
11
  export declare class ClaudeStateDetector extends BaseStateDetector {
12
- detectState(terminal: Terminal): SessionState;
12
+ detectState(terminal: Terminal, currentState: SessionState): SessionState;
13
13
  }
14
14
  export declare class GeminiStateDetector extends BaseStateDetector {
15
- detectState(terminal: Terminal): SessionState;
15
+ detectState(terminal: Terminal, _currentState: SessionState): SessionState;
16
16
  }
17
17
  export declare class CodexStateDetector extends BaseStateDetector {
18
- detectState(terminal: Terminal): SessionState;
18
+ detectState(terminal: Terminal, _currentState: SessionState): SessionState;
19
19
  }
@@ -32,9 +32,13 @@ export class BaseStateDetector {
32
32
  }
33
33
  }
34
34
  export class ClaudeStateDetector extends BaseStateDetector {
35
- detectState(terminal) {
35
+ detectState(terminal, currentState) {
36
36
  const content = this.getTerminalContent(terminal);
37
37
  const lowerContent = content.toLowerCase();
38
+ // Check for ctrl+r toggle prompt - maintain current state
39
+ if (lowerContent.includes('ctrl+r to toggle')) {
40
+ return currentState;
41
+ }
38
42
  // Check for waiting prompts with box character
39
43
  if (content.includes('│ Do you want') ||
40
44
  content.includes('│ Would you like')) {
@@ -50,7 +54,7 @@ export class ClaudeStateDetector extends BaseStateDetector {
50
54
  }
51
55
  // https://github.com/google-gemini/gemini-cli/blob/main/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
52
56
  export class GeminiStateDetector extends BaseStateDetector {
53
- detectState(terminal) {
57
+ detectState(terminal, _currentState) {
54
58
  const content = this.getTerminalContent(terminal);
55
59
  const lowerContent = content.toLowerCase();
56
60
  // Check for waiting prompts with box character
@@ -68,7 +72,7 @@ export class GeminiStateDetector extends BaseStateDetector {
68
72
  }
69
73
  }
70
74
  export class CodexStateDetector extends BaseStateDetector {
71
- detectState(terminal) {
75
+ detectState(terminal, _currentState) {
72
76
  const content = this.getTerminalContent(terminal);
73
77
  const lowerContent = content.toLowerCase();
74
78
  // Check for waiting prompts
@@ -33,7 +33,7 @@ describe('ClaudeStateDetector', () => {
33
33
  '│ > ',
34
34
  ]);
35
35
  // Act
36
- const state = detector.detectState(terminal);
36
+ const state = detector.detectState(terminal, 'idle');
37
37
  // Assert
38
38
  expect(state).toBe('waiting_input');
39
39
  });
@@ -45,7 +45,7 @@ describe('ClaudeStateDetector', () => {
45
45
  '│ > ',
46
46
  ]);
47
47
  // Act
48
- const state = detector.detectState(terminal);
48
+ const state = detector.detectState(terminal, 'idle');
49
49
  // Assert
50
50
  expect(state).toBe('waiting_input');
51
51
  });
@@ -56,7 +56,7 @@ describe('ClaudeStateDetector', () => {
56
56
  'Press ESC to interrupt',
57
57
  ]);
58
58
  // Act
59
- const state = detector.detectState(terminal);
59
+ const state = detector.detectState(terminal, 'idle');
60
60
  // Assert
61
61
  expect(state).toBe('busy');
62
62
  });
@@ -67,7 +67,7 @@ describe('ClaudeStateDetector', () => {
67
67
  'press esc to interrupt the process',
68
68
  ]);
69
69
  // Act
70
- const state = detector.detectState(terminal);
70
+ const state = detector.detectState(terminal, 'idle');
71
71
  // Assert
72
72
  expect(state).toBe('busy');
73
73
  });
@@ -79,7 +79,7 @@ describe('ClaudeStateDetector', () => {
79
79
  '> ',
80
80
  ]);
81
81
  // Act
82
- const state = detector.detectState(terminal);
82
+ const state = detector.detectState(terminal, 'idle');
83
83
  // Assert
84
84
  expect(state).toBe('idle');
85
85
  });
@@ -87,7 +87,7 @@ describe('ClaudeStateDetector', () => {
87
87
  // Arrange
88
88
  terminal = createMockTerminal([]);
89
89
  // Act
90
- const state = detector.detectState(terminal);
90
+ const state = detector.detectState(terminal, 'idle');
91
91
  // Assert
92
92
  expect(state).toBe('idle');
93
93
  });
@@ -106,7 +106,7 @@ describe('ClaudeStateDetector', () => {
106
106
  }
107
107
  terminal = createMockTerminal(lines);
108
108
  // Act
109
- const state = detector.detectState(terminal);
109
+ const state = detector.detectState(terminal, 'idle');
110
110
  // Assert
111
111
  expect(state).toBe('idle'); // Should not detect the old prompt
112
112
  });
@@ -118,10 +118,42 @@ describe('ClaudeStateDetector', () => {
118
118
  '│ > ',
119
119
  ]);
120
120
  // Act
121
- const state = detector.detectState(terminal);
121
+ const state = detector.detectState(terminal, 'idle');
122
122
  // Assert
123
123
  expect(state).toBe('waiting_input'); // waiting_input should take precedence
124
124
  });
125
+ it('should maintain current state when "ctrl+r to toggle" is present', () => {
126
+ // Arrange
127
+ terminal = createMockTerminal([
128
+ 'Some output',
129
+ 'Press Ctrl+R to toggle history search',
130
+ 'More output',
131
+ ]);
132
+ // Act - test with different current states
133
+ const idleState = detector.detectState(terminal, 'idle');
134
+ const busyState = detector.detectState(terminal, 'busy');
135
+ const waitingState = detector.detectState(terminal, 'waiting_input');
136
+ // Assert - should maintain whatever the current state was
137
+ expect(idleState).toBe('idle');
138
+ expect(busyState).toBe('busy');
139
+ expect(waitingState).toBe('waiting_input');
140
+ });
141
+ it('should maintain current state for various "ctrl+r" patterns', () => {
142
+ // Arrange - test different case variations
143
+ const patterns = [
144
+ 'ctrl+r to toggle',
145
+ 'CTRL+R TO TOGGLE',
146
+ 'Ctrl+R to toggle history',
147
+ 'Press ctrl+r to toggle the search',
148
+ ];
149
+ for (const pattern of patterns) {
150
+ terminal = createMockTerminal(['Some output', pattern]);
151
+ // Act
152
+ const state = detector.detectState(terminal, 'busy');
153
+ // Assert - should maintain the current state
154
+ expect(state).toBe('busy');
155
+ }
156
+ });
125
157
  });
126
158
  });
127
159
  describe('GeminiStateDetector', () => {
@@ -157,7 +189,7 @@ describe('GeminiStateDetector', () => {
157
189
  '│ > ',
158
190
  ]);
159
191
  // Act
160
- const state = detector.detectState(terminal);
192
+ const state = detector.detectState(terminal, 'idle');
161
193
  // Assert
162
194
  expect(state).toBe('waiting_input');
163
195
  });
@@ -169,7 +201,7 @@ describe('GeminiStateDetector', () => {
169
201
  '│ > ',
170
202
  ]);
171
203
  // Act
172
- const state = detector.detectState(terminal);
204
+ const state = detector.detectState(terminal, 'idle');
173
205
  // Assert
174
206
  expect(state).toBe('waiting_input');
175
207
  });
@@ -181,7 +213,7 @@ describe('GeminiStateDetector', () => {
181
213
  '│ > ',
182
214
  ]);
183
215
  // Act
184
- const state = detector.detectState(terminal);
216
+ const state = detector.detectState(terminal, 'idle');
185
217
  // Assert
186
218
  expect(state).toBe('waiting_input');
187
219
  });
@@ -192,7 +224,7 @@ describe('GeminiStateDetector', () => {
192
224
  'Press ESC to cancel',
193
225
  ]);
194
226
  // Act
195
- const state = detector.detectState(terminal);
227
+ const state = detector.detectState(terminal, 'idle');
196
228
  // Assert
197
229
  expect(state).toBe('busy');
198
230
  });
@@ -203,7 +235,7 @@ describe('GeminiStateDetector', () => {
203
235
  'Press Esc to cancel the operation',
204
236
  ]);
205
237
  // Act
206
- const state = detector.detectState(terminal);
238
+ const state = detector.detectState(terminal, 'idle');
207
239
  // Assert
208
240
  expect(state).toBe('busy');
209
241
  });
@@ -214,7 +246,7 @@ describe('GeminiStateDetector', () => {
214
246
  'Type your message below',
215
247
  ]);
216
248
  // Act
217
- const state = detector.detectState(terminal);
249
+ const state = detector.detectState(terminal, 'idle');
218
250
  // Assert
219
251
  expect(state).toBe('idle');
220
252
  });
@@ -222,7 +254,7 @@ describe('GeminiStateDetector', () => {
222
254
  // Arrange
223
255
  terminal = createMockTerminal([]);
224
256
  // Act
225
- const state = detector.detectState(terminal);
257
+ const state = detector.detectState(terminal, 'idle');
226
258
  // Assert
227
259
  expect(state).toBe('idle');
228
260
  });
@@ -234,7 +266,7 @@ describe('GeminiStateDetector', () => {
234
266
  '│ > ',
235
267
  ]);
236
268
  // Act
237
- const state = detector.detectState(terminal);
269
+ const state = detector.detectState(terminal, 'idle');
238
270
  // Assert
239
271
  expect(state).toBe('waiting_input'); // waiting_input should take precedence
240
272
  });
@@ -267,7 +299,7 @@ describe('CodexStateDetector', () => {
267
299
  // Arrange
268
300
  terminal = createMockTerminal(['Some output', '│Allow execution?', '│ > ']);
269
301
  // Act
270
- const state = detector.detectState(terminal);
302
+ const state = detector.detectState(terminal, 'idle');
271
303
  // Assert
272
304
  expect(state).toBe('waiting_input');
273
305
  });
@@ -275,7 +307,7 @@ describe('CodexStateDetector', () => {
275
307
  // Arrange
276
308
  terminal = createMockTerminal(['Some output', 'Continue? [y/N]', '> ']);
277
309
  // Act
278
- const state = detector.detectState(terminal);
310
+ const state = detector.detectState(terminal, 'idle');
279
311
  // Assert
280
312
  expect(state).toBe('waiting_input');
281
313
  });
@@ -286,7 +318,7 @@ describe('CodexStateDetector', () => {
286
318
  'Press any key to continue...',
287
319
  ]);
288
320
  // Act
289
- const state = detector.detectState(terminal);
321
+ const state = detector.detectState(terminal, 'idle');
290
322
  // Assert
291
323
  expect(state).toBe('waiting_input');
292
324
  });
@@ -298,7 +330,7 @@ describe('CodexStateDetector', () => {
298
330
  'Working...',
299
331
  ]);
300
332
  // Act
301
- const state = detector.detectState(terminal);
333
+ const state = detector.detectState(terminal, 'idle');
302
334
  // Assert
303
335
  expect(state).toBe('busy');
304
336
  });
@@ -310,7 +342,7 @@ describe('CodexStateDetector', () => {
310
342
  'Working...',
311
343
  ]);
312
344
  // Act
313
- const state = detector.detectState(terminal);
345
+ const state = detector.detectState(terminal, 'idle');
314
346
  // Assert
315
347
  expect(state).toBe('busy');
316
348
  });
@@ -318,7 +350,7 @@ describe('CodexStateDetector', () => {
318
350
  // Arrange
319
351
  terminal = createMockTerminal(['Normal output', 'Some message', 'Ready']);
320
352
  // Act
321
- const state = detector.detectState(terminal);
353
+ const state = detector.detectState(terminal, 'idle');
322
354
  // Assert
323
355
  expect(state).toBe('idle');
324
356
  });
@@ -326,7 +358,7 @@ describe('CodexStateDetector', () => {
326
358
  // Arrange
327
359
  terminal = createMockTerminal(['press esc to cancel', '[y/N]']);
328
360
  // Act
329
- const state = detector.detectState(terminal);
361
+ const state = detector.detectState(terminal, 'idle');
330
362
  // Assert
331
363
  expect(state).toBe('waiting_input');
332
364
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",