ccmanager 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +57 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +24 -0
- package/dist/components/App.d.ts +3 -0
- package/dist/components/App.js +228 -0
- package/dist/components/ConfigureShortcuts.d.ts +6 -0
- package/dist/components/ConfigureShortcuts.js +139 -0
- package/dist/components/Confirmation.d.ts +12 -0
- package/dist/components/Confirmation.js +42 -0
- package/dist/components/DeleteWorktree.d.ts +7 -0
- package/dist/components/DeleteWorktree.js +116 -0
- package/dist/components/Menu.d.ts +9 -0
- package/dist/components/Menu.js +154 -0
- package/dist/components/MergeWorktree.d.ts +7 -0
- package/dist/components/MergeWorktree.js +142 -0
- package/dist/components/NewWorktree.d.ts +7 -0
- package/dist/components/NewWorktree.js +49 -0
- package/dist/components/Session.d.ts +10 -0
- package/dist/components/Session.js +121 -0
- package/dist/constants/statusIcons.d.ts +18 -0
- package/dist/constants/statusIcons.js +27 -0
- package/dist/services/sessionManager.d.ts +16 -0
- package/dist/services/sessionManager.js +190 -0
- package/dist/services/sessionManager.test.d.ts +1 -0
- package/dist/services/sessionManager.test.js +99 -0
- package/dist/services/shortcutManager.d.ts +17 -0
- package/dist/services/shortcutManager.js +167 -0
- package/dist/services/worktreeService.d.ts +24 -0
- package/dist/services/worktreeService.js +220 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/promptDetector.d.ts +1 -0
- package/dist/utils/promptDetector.js +20 -0
- package/dist/utils/promptDetector.test.d.ts +1 -0
- package/dist/utils/promptDetector.test.js +81 -0
- package/package.json +70 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { WorktreeService } from '../services/worktreeService.js';
|
|
4
|
+
import Confirmation from './Confirmation.js';
|
|
5
|
+
import { shortcutManager } from '../services/shortcutManager.js';
|
|
6
|
+
const DeleteWorktree = ({ onComplete, onCancel, }) => {
|
|
7
|
+
const [worktrees, setWorktrees] = useState([]);
|
|
8
|
+
const [selectedIndices, setSelectedIndices] = useState(new Set());
|
|
9
|
+
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
10
|
+
const [confirmMode, setConfirmMode] = useState(false);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const worktreeService = new WorktreeService();
|
|
13
|
+
const allWorktrees = worktreeService.getWorktrees();
|
|
14
|
+
// Filter out main worktree - we shouldn't delete it
|
|
15
|
+
const deletableWorktrees = allWorktrees.filter(wt => !wt.isMainWorktree);
|
|
16
|
+
setWorktrees(deletableWorktrees);
|
|
17
|
+
}, []);
|
|
18
|
+
useInput((input, key) => {
|
|
19
|
+
if (key.ctrl && input === 'c') {
|
|
20
|
+
onCancel();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (confirmMode) {
|
|
24
|
+
// Confirmation component handles input
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (key.upArrow) {
|
|
28
|
+
setFocusedIndex(prev => Math.max(0, prev - 1));
|
|
29
|
+
}
|
|
30
|
+
else if (key.downArrow) {
|
|
31
|
+
setFocusedIndex(prev => Math.min(worktrees.length - 1, prev + 1));
|
|
32
|
+
}
|
|
33
|
+
else if (input === ' ') {
|
|
34
|
+
// Toggle selection
|
|
35
|
+
setSelectedIndices(prev => {
|
|
36
|
+
const newSet = new Set(prev);
|
|
37
|
+
if (newSet.has(focusedIndex)) {
|
|
38
|
+
newSet.delete(focusedIndex);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
newSet.add(focusedIndex);
|
|
42
|
+
}
|
|
43
|
+
return newSet;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else if (key.return) {
|
|
47
|
+
if (selectedIndices.size > 0) {
|
|
48
|
+
setConfirmMode(true);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (shortcutManager.matchesShortcut('cancel', input, key)) {
|
|
52
|
+
onCancel();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
if (worktrees.length === 0) {
|
|
56
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
57
|
+
React.createElement(Text, { color: "yellow" }, "No worktrees available to delete."),
|
|
58
|
+
React.createElement(Text, { dimColor: true },
|
|
59
|
+
"Press ",
|
|
60
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
61
|
+
" to return to menu")));
|
|
62
|
+
}
|
|
63
|
+
if (confirmMode) {
|
|
64
|
+
const selectedWorktrees = Array.from(selectedIndices).map(index => worktrees[index]);
|
|
65
|
+
const handleConfirm = () => {
|
|
66
|
+
const selectedPaths = Array.from(selectedIndices).map(index => worktrees[index].path);
|
|
67
|
+
onComplete(selectedPaths);
|
|
68
|
+
};
|
|
69
|
+
const handleCancel = () => {
|
|
70
|
+
setConfirmMode(false);
|
|
71
|
+
};
|
|
72
|
+
const confirmMessage = (React.createElement(Box, { flexDirection: "column" },
|
|
73
|
+
React.createElement(Text, { bold: true, color: "red" }, "\u26A0\uFE0F Delete Confirmation"),
|
|
74
|
+
React.createElement(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column" },
|
|
75
|
+
React.createElement(Text, null, "You are about to delete the following worktrees:"),
|
|
76
|
+
selectedWorktrees.map(wt => (React.createElement(Text, { key: wt.path, color: "red" },
|
|
77
|
+
"\u2022 ",
|
|
78
|
+
wt.branch.replace('refs/heads/', ''),
|
|
79
|
+
" (",
|
|
80
|
+
wt.path,
|
|
81
|
+
")")))),
|
|
82
|
+
React.createElement(Text, { bold: true }, "This will also delete their branches. Are you sure?")));
|
|
83
|
+
return (React.createElement(Confirmation, { message: confirmMessage, onConfirm: handleConfirm, onCancel: handleCancel }));
|
|
84
|
+
}
|
|
85
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
86
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
87
|
+
React.createElement(Text, { bold: true, color: "red" }, "Delete Worktrees")),
|
|
88
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
89
|
+
React.createElement(Text, { dimColor: true }, "Select worktrees to delete (Space to select, Enter to confirm):")),
|
|
90
|
+
worktrees.map((worktree, index) => {
|
|
91
|
+
const isSelected = selectedIndices.has(index);
|
|
92
|
+
const isFocused = index === focusedIndex;
|
|
93
|
+
const branchName = worktree.branch.replace('refs/heads/', '');
|
|
94
|
+
return (React.createElement(Box, { key: worktree.path },
|
|
95
|
+
React.createElement(Text, { color: isFocused ? 'green' : undefined, inverse: isFocused, dimColor: !isFocused && !isSelected },
|
|
96
|
+
isSelected ? '[✓]' : '[ ]',
|
|
97
|
+
" ",
|
|
98
|
+
branchName,
|
|
99
|
+
" (",
|
|
100
|
+
worktree.path,
|
|
101
|
+
")")));
|
|
102
|
+
}),
|
|
103
|
+
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
104
|
+
React.createElement(Text, { dimColor: true },
|
|
105
|
+
"Controls: \u2191\u2193 Navigate, Space Select, Enter Confirm,",
|
|
106
|
+
' ',
|
|
107
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
108
|
+
" Cancel"),
|
|
109
|
+
selectedIndices.size > 0 && (React.createElement(Text, { color: "yellow" },
|
|
110
|
+
selectedIndices.size,
|
|
111
|
+
" worktree",
|
|
112
|
+
selectedIndices.size > 1 ? 's' : '',
|
|
113
|
+
' ',
|
|
114
|
+
"selected")))));
|
|
115
|
+
};
|
|
116
|
+
export default DeleteWorktree;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Worktree } from '../types/index.js';
|
|
3
|
+
import { SessionManager } from '../services/sessionManager.js';
|
|
4
|
+
interface MenuProps {
|
|
5
|
+
sessionManager: SessionManager;
|
|
6
|
+
onSelectWorktree: (worktree: Worktree) => void;
|
|
7
|
+
}
|
|
8
|
+
declare const Menu: React.FC<MenuProps>;
|
|
9
|
+
export default Menu;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { WorktreeService } from '../services/worktreeService.js';
|
|
5
|
+
import { STATUS_ICONS, STATUS_LABELS, MENU_ICONS, getStatusDisplay, } from '../constants/statusIcons.js';
|
|
6
|
+
const Menu = ({ sessionManager, onSelectWorktree }) => {
|
|
7
|
+
const [worktrees, setWorktrees] = useState([]);
|
|
8
|
+
const [sessions, setSessions] = useState([]);
|
|
9
|
+
const [items, setItems] = useState([]);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
// Load worktrees
|
|
12
|
+
const worktreeService = new WorktreeService();
|
|
13
|
+
const loadedWorktrees = worktreeService.getWorktrees();
|
|
14
|
+
setWorktrees(loadedWorktrees);
|
|
15
|
+
// Update sessions
|
|
16
|
+
const updateSessions = () => {
|
|
17
|
+
const allSessions = sessionManager.getAllSessions();
|
|
18
|
+
setSessions(allSessions);
|
|
19
|
+
// Update worktree session status
|
|
20
|
+
loadedWorktrees.forEach(wt => {
|
|
21
|
+
wt.hasSession = allSessions.some(s => s.worktreePath === wt.path);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
updateSessions();
|
|
25
|
+
// Listen for session changes
|
|
26
|
+
const handleSessionChange = () => updateSessions();
|
|
27
|
+
sessionManager.on('sessionCreated', handleSessionChange);
|
|
28
|
+
sessionManager.on('sessionDestroyed', handleSessionChange);
|
|
29
|
+
sessionManager.on('sessionStateChanged', handleSessionChange);
|
|
30
|
+
return () => {
|
|
31
|
+
sessionManager.off('sessionCreated', handleSessionChange);
|
|
32
|
+
sessionManager.off('sessionDestroyed', handleSessionChange);
|
|
33
|
+
sessionManager.off('sessionStateChanged', handleSessionChange);
|
|
34
|
+
};
|
|
35
|
+
}, [sessionManager]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
// Build menu items
|
|
38
|
+
const menuItems = worktrees.map(wt => {
|
|
39
|
+
const session = sessions.find(s => s.worktreePath === wt.path);
|
|
40
|
+
let status = '';
|
|
41
|
+
if (session) {
|
|
42
|
+
status = ` [${getStatusDisplay(session.state)}]`;
|
|
43
|
+
}
|
|
44
|
+
const branchName = wt.branch.replace('refs/heads/', '');
|
|
45
|
+
const isMain = wt.isMainWorktree ? ' (main)' : '';
|
|
46
|
+
return {
|
|
47
|
+
label: `${branchName}${isMain}${status}`,
|
|
48
|
+
value: wt.path,
|
|
49
|
+
worktree: wt,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
// Add menu options
|
|
53
|
+
menuItems.push({
|
|
54
|
+
label: '─────────────',
|
|
55
|
+
value: 'separator',
|
|
56
|
+
});
|
|
57
|
+
menuItems.push({
|
|
58
|
+
label: `${MENU_ICONS.NEW_WORKTREE} New Worktree`,
|
|
59
|
+
value: 'new-worktree',
|
|
60
|
+
});
|
|
61
|
+
menuItems.push({
|
|
62
|
+
label: `${MENU_ICONS.MERGE_WORKTREE} Merge Worktree`,
|
|
63
|
+
value: 'merge-worktree',
|
|
64
|
+
});
|
|
65
|
+
menuItems.push({
|
|
66
|
+
label: `${MENU_ICONS.DELETE_WORKTREE} Delete Worktree`,
|
|
67
|
+
value: 'delete-worktree',
|
|
68
|
+
});
|
|
69
|
+
menuItems.push({
|
|
70
|
+
label: `${MENU_ICONS.CONFIGURE_SHORTCUTS} Configure Shortcuts`,
|
|
71
|
+
value: 'configure-shortcuts',
|
|
72
|
+
});
|
|
73
|
+
menuItems.push({
|
|
74
|
+
label: `${MENU_ICONS.EXIT} Exit`,
|
|
75
|
+
value: 'exit',
|
|
76
|
+
});
|
|
77
|
+
setItems(menuItems);
|
|
78
|
+
}, [worktrees, sessions]);
|
|
79
|
+
const handleSelect = (item) => {
|
|
80
|
+
if (item.value === 'separator') {
|
|
81
|
+
// Do nothing for separator
|
|
82
|
+
}
|
|
83
|
+
else if (item.value === 'new-worktree') {
|
|
84
|
+
// Handle in parent component
|
|
85
|
+
onSelectWorktree({
|
|
86
|
+
path: '',
|
|
87
|
+
branch: '',
|
|
88
|
+
isMainWorktree: false,
|
|
89
|
+
hasSession: false,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else if (item.value === 'merge-worktree') {
|
|
93
|
+
// Handle in parent component - use special marker
|
|
94
|
+
onSelectWorktree({
|
|
95
|
+
path: 'MERGE_WORKTREE',
|
|
96
|
+
branch: '',
|
|
97
|
+
isMainWorktree: false,
|
|
98
|
+
hasSession: false,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else if (item.value === 'delete-worktree') {
|
|
102
|
+
// Handle in parent component - use special marker
|
|
103
|
+
onSelectWorktree({
|
|
104
|
+
path: 'DELETE_WORKTREE',
|
|
105
|
+
branch: '',
|
|
106
|
+
isMainWorktree: false,
|
|
107
|
+
hasSession: false,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else if (item.value === 'configure-shortcuts') {
|
|
111
|
+
// Handle in parent component - use special marker
|
|
112
|
+
onSelectWorktree({
|
|
113
|
+
path: 'CONFIGURE_SHORTCUTS',
|
|
114
|
+
branch: '',
|
|
115
|
+
isMainWorktree: false,
|
|
116
|
+
hasSession: false,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else if (item.value === 'exit') {
|
|
120
|
+
// Handle in parent component - use special marker
|
|
121
|
+
onSelectWorktree({
|
|
122
|
+
path: 'EXIT_APPLICATION',
|
|
123
|
+
branch: '',
|
|
124
|
+
isMainWorktree: false,
|
|
125
|
+
hasSession: false,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else if (item.worktree) {
|
|
129
|
+
onSelectWorktree(item.worktree);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
133
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
134
|
+
React.createElement(Text, { bold: true, color: "green" }, "CCManager - Claude Code Worktree Manager")),
|
|
135
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
136
|
+
React.createElement(Text, { dimColor: true }, "Select a worktree to start or resume a Claude Code session:")),
|
|
137
|
+
React.createElement(SelectInput, { items: items, onSelect: handleSelect, isFocused: true }),
|
|
138
|
+
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
139
|
+
React.createElement(Text, { dimColor: true },
|
|
140
|
+
"Status: ",
|
|
141
|
+
STATUS_ICONS.BUSY,
|
|
142
|
+
" ",
|
|
143
|
+
STATUS_LABELS.BUSY,
|
|
144
|
+
' ',
|
|
145
|
+
STATUS_ICONS.WAITING,
|
|
146
|
+
" ",
|
|
147
|
+
STATUS_LABELS.WAITING,
|
|
148
|
+
" ",
|
|
149
|
+
STATUS_ICONS.IDLE,
|
|
150
|
+
' ',
|
|
151
|
+
STATUS_LABELS.IDLE),
|
|
152
|
+
React.createElement(Text, { dimColor: true }, "Controls: \u2191\u2193 Navigate Enter Select"))));
|
|
153
|
+
};
|
|
154
|
+
export default Menu;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface MergeWorktreeProps {
|
|
3
|
+
onComplete: (sourceBranch: string, targetBranch: string, deleteAfterMerge: boolean, useRebase: boolean) => void;
|
|
4
|
+
onCancel: () => void;
|
|
5
|
+
}
|
|
6
|
+
declare const MergeWorktree: React.FC<MergeWorktreeProps>;
|
|
7
|
+
export default MergeWorktree;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { WorktreeService } from '../services/worktreeService.js';
|
|
5
|
+
import Confirmation from './Confirmation.js';
|
|
6
|
+
import { shortcutManager } from '../services/shortcutManager.js';
|
|
7
|
+
const MergeWorktree = ({ onComplete, onCancel, }) => {
|
|
8
|
+
const [step, setStep] = useState('select-source');
|
|
9
|
+
const [sourceBranch, setSourceBranch] = useState('');
|
|
10
|
+
const [targetBranch, setTargetBranch] = useState('');
|
|
11
|
+
const [branchItems, setBranchItems] = useState([]);
|
|
12
|
+
const [useRebase, setUseRebase] = useState(false);
|
|
13
|
+
const [operationFocused, setOperationFocused] = useState(false);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const worktreeService = new WorktreeService();
|
|
16
|
+
const loadedWorktrees = worktreeService.getWorktrees();
|
|
17
|
+
// Create branch items for selection
|
|
18
|
+
const items = loadedWorktrees.map(wt => ({
|
|
19
|
+
label: wt.branch.replace('refs/heads/', '') +
|
|
20
|
+
(wt.isMainWorktree ? ' (main)' : ''),
|
|
21
|
+
value: wt.branch.replace('refs/heads/', ''),
|
|
22
|
+
}));
|
|
23
|
+
setBranchItems(items);
|
|
24
|
+
}, []);
|
|
25
|
+
useInput((input, key) => {
|
|
26
|
+
if (shortcutManager.matchesShortcut('cancel', input, key)) {
|
|
27
|
+
onCancel();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (step === 'select-operation') {
|
|
31
|
+
if (key.leftArrow || key.rightArrow) {
|
|
32
|
+
const newOperationFocused = !operationFocused;
|
|
33
|
+
setOperationFocused(newOperationFocused);
|
|
34
|
+
setUseRebase(newOperationFocused);
|
|
35
|
+
}
|
|
36
|
+
else if (key.return) {
|
|
37
|
+
setStep('confirm-merge');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const handleSelectSource = (item) => {
|
|
42
|
+
setSourceBranch(item.value);
|
|
43
|
+
// Filter out the selected source branch for target selection
|
|
44
|
+
const filteredItems = branchItems.filter(b => b.value !== item.value);
|
|
45
|
+
setBranchItems(filteredItems);
|
|
46
|
+
setStep('select-target');
|
|
47
|
+
};
|
|
48
|
+
const handleSelectTarget = (item) => {
|
|
49
|
+
setTargetBranch(item.value);
|
|
50
|
+
setStep('select-operation');
|
|
51
|
+
};
|
|
52
|
+
if (step === 'select-source') {
|
|
53
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
54
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
55
|
+
React.createElement(Text, { bold: true, color: "green" }, "Merge Worktree")),
|
|
56
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
57
|
+
React.createElement(Text, null, "Select the source branch to merge:")),
|
|
58
|
+
React.createElement(SelectInput, { items: branchItems, onSelect: handleSelectSource, isFocused: true }),
|
|
59
|
+
React.createElement(Box, { marginTop: 1 },
|
|
60
|
+
React.createElement(Text, { dimColor: true },
|
|
61
|
+
"Press ",
|
|
62
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
63
|
+
" to cancel"))));
|
|
64
|
+
}
|
|
65
|
+
if (step === 'select-target') {
|
|
66
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
67
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
68
|
+
React.createElement(Text, { bold: true, color: "green" }, "Merge Worktree")),
|
|
69
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
70
|
+
React.createElement(Text, null,
|
|
71
|
+
"Merging from: ",
|
|
72
|
+
React.createElement(Text, { color: "yellow" }, sourceBranch))),
|
|
73
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
74
|
+
React.createElement(Text, null, "Select the target branch to merge into:")),
|
|
75
|
+
React.createElement(SelectInput, { items: branchItems, onSelect: handleSelectTarget, isFocused: true }),
|
|
76
|
+
React.createElement(Box, { marginTop: 1 },
|
|
77
|
+
React.createElement(Text, { dimColor: true },
|
|
78
|
+
"Press ",
|
|
79
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
80
|
+
" to cancel"))));
|
|
81
|
+
}
|
|
82
|
+
if (step === 'select-operation') {
|
|
83
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
84
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
85
|
+
React.createElement(Text, { bold: true, color: "green" }, "Select Operation")),
|
|
86
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
87
|
+
React.createElement(Text, null,
|
|
88
|
+
"Choose how to integrate ",
|
|
89
|
+
React.createElement(Text, { color: "yellow" }, sourceBranch),
|
|
90
|
+
' ',
|
|
91
|
+
"into ",
|
|
92
|
+
React.createElement(Text, { color: "yellow" }, targetBranch),
|
|
93
|
+
":")),
|
|
94
|
+
React.createElement(Box, null,
|
|
95
|
+
React.createElement(Box, { marginRight: 2 },
|
|
96
|
+
React.createElement(Text, { color: !operationFocused ? 'green' : 'white', inverse: !operationFocused },
|
|
97
|
+
' ',
|
|
98
|
+
"Merge",
|
|
99
|
+
' ')),
|
|
100
|
+
React.createElement(Box, null,
|
|
101
|
+
React.createElement(Text, { color: operationFocused ? 'blue' : 'white', inverse: operationFocused },
|
|
102
|
+
' ',
|
|
103
|
+
"Rebase",
|
|
104
|
+
' '))),
|
|
105
|
+
React.createElement(Box, { marginTop: 1 },
|
|
106
|
+
React.createElement(Text, { dimColor: true },
|
|
107
|
+
"Use \u2190 \u2192 to navigate, Enter to select,",
|
|
108
|
+
' ',
|
|
109
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
110
|
+
" to cancel"))));
|
|
111
|
+
}
|
|
112
|
+
if (step === 'confirm-merge') {
|
|
113
|
+
const confirmMessage = (React.createElement(Box, { flexDirection: "column" },
|
|
114
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
115
|
+
React.createElement(Text, { bold: true, color: "green" },
|
|
116
|
+
"Confirm ",
|
|
117
|
+
useRebase ? 'Rebase' : 'Merge')),
|
|
118
|
+
React.createElement(Text, null,
|
|
119
|
+
useRebase ? 'Rebase' : 'Merge',
|
|
120
|
+
' ',
|
|
121
|
+
React.createElement(Text, { color: "yellow" }, sourceBranch),
|
|
122
|
+
' ',
|
|
123
|
+
useRebase ? 'onto' : 'into',
|
|
124
|
+
' ',
|
|
125
|
+
React.createElement(Text, { color: "yellow" }, targetBranch),
|
|
126
|
+
"?")));
|
|
127
|
+
return (React.createElement(Confirmation, { message: confirmMessage, onConfirm: () => setStep('delete-confirm'), onCancel: onCancel }));
|
|
128
|
+
}
|
|
129
|
+
if (step === 'delete-confirm') {
|
|
130
|
+
const deleteMessage = (React.createElement(Box, { flexDirection: "column" },
|
|
131
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
132
|
+
React.createElement(Text, { bold: true, color: "green" }, "Delete Source Branch?")),
|
|
133
|
+
React.createElement(Text, null,
|
|
134
|
+
"Delete the merged branch ",
|
|
135
|
+
React.createElement(Text, { color: "yellow" }, sourceBranch),
|
|
136
|
+
' ',
|
|
137
|
+
"and its worktree?")));
|
|
138
|
+
return (React.createElement(Confirmation, { message: deleteMessage, onConfirm: () => onComplete(sourceBranch, targetBranch, true, useRebase), onCancel: () => onComplete(sourceBranch, targetBranch, false, useRebase) }));
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
};
|
|
142
|
+
export default MergeWorktree;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { shortcutManager } from '../services/shortcutManager.js';
|
|
5
|
+
const NewWorktree = ({ onComplete, onCancel }) => {
|
|
6
|
+
const [step, setStep] = useState('path');
|
|
7
|
+
const [path, setPath] = useState('');
|
|
8
|
+
const [branch, setBranch] = useState('');
|
|
9
|
+
useInput((input, key) => {
|
|
10
|
+
if (shortcutManager.matchesShortcut('cancel', input, key)) {
|
|
11
|
+
onCancel();
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
const handlePathSubmit = (value) => {
|
|
15
|
+
if (value.trim()) {
|
|
16
|
+
setPath(value.trim());
|
|
17
|
+
setStep('branch');
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const handleBranchSubmit = (value) => {
|
|
21
|
+
if (value.trim()) {
|
|
22
|
+
setBranch(value.trim());
|
|
23
|
+
onComplete(path, value.trim());
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
27
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
28
|
+
React.createElement(Text, { bold: true, color: "green" }, "Create New Worktree")),
|
|
29
|
+
step === 'path' ? (React.createElement(Box, { flexDirection: "column" },
|
|
30
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
31
|
+
React.createElement(Text, null, "Enter worktree path (relative to repository root):")),
|
|
32
|
+
React.createElement(Box, null,
|
|
33
|
+
React.createElement(Text, { color: "cyan" }, '> '),
|
|
34
|
+
React.createElement(TextInput, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })))) : (React.createElement(Box, { flexDirection: "column" },
|
|
35
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
36
|
+
React.createElement(Text, null,
|
|
37
|
+
"Enter branch name for worktree at ",
|
|
38
|
+
React.createElement(Text, { color: "cyan" }, path),
|
|
39
|
+
":")),
|
|
40
|
+
React.createElement(Box, null,
|
|
41
|
+
React.createElement(Text, { color: "cyan" }, '> '),
|
|
42
|
+
React.createElement(TextInput, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })))),
|
|
43
|
+
React.createElement(Box, { marginTop: 1 },
|
|
44
|
+
React.createElement(Text, { dimColor: true },
|
|
45
|
+
"Press ",
|
|
46
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
47
|
+
" to cancel"))));
|
|
48
|
+
};
|
|
49
|
+
export default NewWorktree;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Session as SessionType } from '../types/index.js';
|
|
3
|
+
import { SessionManager } from '../services/sessionManager.js';
|
|
4
|
+
interface SessionProps {
|
|
5
|
+
session: SessionType;
|
|
6
|
+
sessionManager: SessionManager;
|
|
7
|
+
onReturnToMenu: () => void;
|
|
8
|
+
}
|
|
9
|
+
declare const Session: React.FC<SessionProps>;
|
|
10
|
+
export default Session;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useStdout } from 'ink';
|
|
3
|
+
import { shortcutManager } from '../services/shortcutManager.js';
|
|
4
|
+
const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
5
|
+
const { stdout } = useStdout();
|
|
6
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (!stdout)
|
|
9
|
+
return;
|
|
10
|
+
// Clear screen when entering session
|
|
11
|
+
stdout.write('\x1B[2J\x1B[H');
|
|
12
|
+
// Handle session restoration
|
|
13
|
+
const handleSessionRestore = (restoredSession) => {
|
|
14
|
+
if (restoredSession.id === session.id) {
|
|
15
|
+
// Replay all buffered output, but skip the initial clear if present
|
|
16
|
+
for (let i = 0; i < restoredSession.outputHistory.length; i++) {
|
|
17
|
+
const buffer = restoredSession.outputHistory[i];
|
|
18
|
+
if (!buffer)
|
|
19
|
+
continue;
|
|
20
|
+
const str = buffer.toString('utf8');
|
|
21
|
+
// Skip clear screen sequences at the beginning
|
|
22
|
+
if (i === 0 && (str.includes('\x1B[2J') || str.includes('\x1B[H'))) {
|
|
23
|
+
// Skip this buffer or remove the clear sequence
|
|
24
|
+
const cleaned = str
|
|
25
|
+
.replace(/\x1B\[2J/g, '')
|
|
26
|
+
.replace(/\x1B\[H/g, '');
|
|
27
|
+
if (cleaned.length > 0) {
|
|
28
|
+
stdout.write(Buffer.from(cleaned, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
stdout.write(buffer);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
// Listen for restore event first
|
|
38
|
+
sessionManager.on('sessionRestore', handleSessionRestore);
|
|
39
|
+
// Mark session as active (this will trigger the restore event)
|
|
40
|
+
sessionManager.setSessionActive(session.worktreePath, true);
|
|
41
|
+
// Listen for session data events
|
|
42
|
+
const handleSessionData = (activeSession, data) => {
|
|
43
|
+
// Only handle data for our session
|
|
44
|
+
if (activeSession.id === session.id && !isExiting) {
|
|
45
|
+
stdout.write(data);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const handleSessionExit = (exitedSession) => {
|
|
49
|
+
if (exitedSession.id === session.id) {
|
|
50
|
+
setIsExiting(true);
|
|
51
|
+
// Don't call onReturnToMenu here - App component handles it
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
sessionManager.on('sessionData', handleSessionData);
|
|
55
|
+
sessionManager.on('sessionExit', handleSessionExit);
|
|
56
|
+
// Handle terminal resize
|
|
57
|
+
const handleResize = () => {
|
|
58
|
+
session.process.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
59
|
+
};
|
|
60
|
+
stdout.on('resize', handleResize);
|
|
61
|
+
// Set up raw input handling
|
|
62
|
+
const stdin = process.stdin;
|
|
63
|
+
// Store original stdin state
|
|
64
|
+
const originalIsRaw = stdin.isRaw;
|
|
65
|
+
const originalIsPaused = stdin.isPaused();
|
|
66
|
+
// Configure stdin for PTY passthrough
|
|
67
|
+
stdin.setRawMode(true);
|
|
68
|
+
stdin.resume();
|
|
69
|
+
stdin.setEncoding('utf8');
|
|
70
|
+
const handleStdinData = (data) => {
|
|
71
|
+
if (isExiting)
|
|
72
|
+
return;
|
|
73
|
+
// Check for return to menu shortcut
|
|
74
|
+
const returnToMenuShortcut = shortcutManager.getShortcuts().returnToMenu;
|
|
75
|
+
const shortcutCode = shortcutManager.getShortcutCode(returnToMenuShortcut);
|
|
76
|
+
if (shortcutCode && data === shortcutCode) {
|
|
77
|
+
// Disable focus reporting mode before returning to menu
|
|
78
|
+
if (stdout) {
|
|
79
|
+
stdout.write('\x1b[?1004l');
|
|
80
|
+
}
|
|
81
|
+
// Restore stdin state before returning to menu
|
|
82
|
+
stdin.removeListener('data', handleStdinData);
|
|
83
|
+
stdin.setRawMode(false);
|
|
84
|
+
stdin.pause();
|
|
85
|
+
onReturnToMenu();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Pass all other input directly to the PTY
|
|
89
|
+
session.process.write(data);
|
|
90
|
+
};
|
|
91
|
+
stdin.on('data', handleStdinData);
|
|
92
|
+
return () => {
|
|
93
|
+
// Remove listener first to prevent any race conditions
|
|
94
|
+
stdin.removeListener('data', handleStdinData);
|
|
95
|
+
// Disable focus reporting mode that might have been enabled by the PTY
|
|
96
|
+
if (stdout) {
|
|
97
|
+
stdout.write('\x1b[?1004l');
|
|
98
|
+
}
|
|
99
|
+
// Restore stdin to its original state
|
|
100
|
+
if (stdin.isTTY) {
|
|
101
|
+
stdin.setRawMode(originalIsRaw || false);
|
|
102
|
+
if (originalIsPaused) {
|
|
103
|
+
stdin.pause();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
stdin.resume();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Mark session as inactive
|
|
110
|
+
sessionManager.setSessionActive(session.worktreePath, false);
|
|
111
|
+
// Remove event listeners
|
|
112
|
+
sessionManager.off('sessionRestore', handleSessionRestore);
|
|
113
|
+
sessionManager.off('sessionData', handleSessionData);
|
|
114
|
+
sessionManager.off('sessionExit', handleSessionExit);
|
|
115
|
+
stdout.off('resize', handleResize);
|
|
116
|
+
};
|
|
117
|
+
}, [session, sessionManager, stdout, onReturnToMenu, isExiting]);
|
|
118
|
+
// Return null to render nothing (PTY output goes directly to stdout)
|
|
119
|
+
return null;
|
|
120
|
+
};
|
|
121
|
+
export default Session;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const STATUS_ICONS: {
|
|
2
|
+
readonly BUSY: "●";
|
|
3
|
+
readonly WAITING: "◐";
|
|
4
|
+
readonly IDLE: "○";
|
|
5
|
+
};
|
|
6
|
+
export declare const STATUS_LABELS: {
|
|
7
|
+
readonly BUSY: "Busy";
|
|
8
|
+
readonly WAITING: "Waiting";
|
|
9
|
+
readonly IDLE: "Idle";
|
|
10
|
+
};
|
|
11
|
+
export declare const MENU_ICONS: {
|
|
12
|
+
readonly NEW_WORKTREE: "⊕";
|
|
13
|
+
readonly MERGE_WORKTREE: "⇄";
|
|
14
|
+
readonly DELETE_WORKTREE: "✕";
|
|
15
|
+
readonly CONFIGURE_SHORTCUTS: "⌨";
|
|
16
|
+
readonly EXIT: "⏻";
|
|
17
|
+
};
|
|
18
|
+
export declare const getStatusDisplay: (status: "busy" | "waiting_input" | "idle") => string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const STATUS_ICONS = {
|
|
2
|
+
BUSY: '●',
|
|
3
|
+
WAITING: '◐',
|
|
4
|
+
IDLE: '○',
|
|
5
|
+
};
|
|
6
|
+
export const STATUS_LABELS = {
|
|
7
|
+
BUSY: 'Busy',
|
|
8
|
+
WAITING: 'Waiting',
|
|
9
|
+
IDLE: 'Idle',
|
|
10
|
+
};
|
|
11
|
+
export const MENU_ICONS = {
|
|
12
|
+
NEW_WORKTREE: '⊕',
|
|
13
|
+
MERGE_WORKTREE: '⇄',
|
|
14
|
+
DELETE_WORKTREE: '✕',
|
|
15
|
+
CONFIGURE_SHORTCUTS: '⌨',
|
|
16
|
+
EXIT: '⏻',
|
|
17
|
+
};
|
|
18
|
+
export const getStatusDisplay = (status) => {
|
|
19
|
+
switch (status) {
|
|
20
|
+
case 'busy':
|
|
21
|
+
return `${STATUS_ICONS.BUSY} ${STATUS_LABELS.BUSY}`;
|
|
22
|
+
case 'waiting_input':
|
|
23
|
+
return `${STATUS_ICONS.WAITING} ${STATUS_LABELS.WAITING}`;
|
|
24
|
+
case 'idle':
|
|
25
|
+
return `${STATUS_ICONS.IDLE} ${STATUS_LABELS.IDLE}`;
|
|
26
|
+
}
|
|
27
|
+
};
|