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 +3 -5
- package/dist/components/ConfigureCommand.js +13 -23
- package/dist/components/Confirmation.d.ts +25 -2
- package/dist/components/Confirmation.js +52 -31
- package/dist/components/DeleteConfirmation.js +70 -67
- package/dist/components/MergeWorktree.js +25 -42
- package/dist/services/sessionManager.js +1 -1
- package/dist/services/stateDetector.d.ts +5 -5
- package/dist/services/stateDetector.js +7 -3
- package/dist/services/stateDetector.test.js +55 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
# CCManager - AI Code
|
|
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
|
+
[](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
|
|
358
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
12
|
-
export default Confirmation;
|
|
35
|
+
export declare const SimpleConfirmation: React.FC<SimpleConfirmationProps>;
|
|
@@ -1,42 +1,63 @@
|
|
|
1
|
-
import 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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
10
|
+
// Check custom input handler first
|
|
11
|
+
if (onCustomInput && onCustomInput(input, key)) {
|
|
12
|
+
return;
|
|
9
13
|
}
|
|
10
|
-
|
|
11
|
-
|
|
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 },
|
|
24
|
-
React.createElement(Box,
|
|
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, {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 = (
|
|
32
|
-
if (
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 (
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
")")))
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
React.createElement(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
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(
|
|
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
|
});
|