ccmanager 3.12.0 → 3.12.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/dist/components/Dashboard.js +8 -6
- package/dist/components/DeleteWorktree.js +42 -19
- package/dist/components/Menu.js +22 -29
- package/dist/components/NewWorktree.js +2 -1
- package/dist/components/SearchableList.d.ts +22 -0
- package/dist/components/SearchableList.js +14 -0
- package/dist/hooks/useDynamicLimit.d.ts +10 -0
- package/dist/hooks/useDynamicLimit.js +9 -0
- package/dist/utils/filterByQuery.d.ts +5 -0
- package/dist/utils/filterByQuery.js +13 -0
- package/package.json +6 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
-
import { Box, Text, useInput
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { Effect } from 'effect';
|
|
5
5
|
import SelectInput from 'ink-select-input';
|
|
6
6
|
import stripAnsi from 'strip-ansi';
|
|
@@ -10,10 +10,11 @@ import { SessionManager } from '../services/sessionManager.js';
|
|
|
10
10
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
11
11
|
import { STATUS_ICONS, STATUS_LABELS, MENU_ICONS, getStatusDisplay, } from '../constants/statusIcons.js';
|
|
12
12
|
import { useSearchMode } from '../hooks/useSearchMode.js';
|
|
13
|
+
import { useDynamicLimit } from '../hooks/useDynamicLimit.js';
|
|
13
14
|
import { useGitStatus } from '../hooks/useGitStatus.js';
|
|
14
15
|
import { truncateString, calculateColumnPositions, assembleWorktreeLabel, formatRelativeDate, } from '../utils/worktreeUtils.js';
|
|
15
16
|
import { formatGitFileChanges, formatGitAheadBehind, formatParentBranch, } from '../utils/gitStatus.js';
|
|
16
|
-
import
|
|
17
|
+
import SearchableList from './SearchableList.js';
|
|
17
18
|
const MAX_BRANCH_NAME_LENGTH = 70;
|
|
18
19
|
const createSeparatorWithText = (text, totalWidth = 35) => {
|
|
19
20
|
const textWithSpaces = ` ${text} `;
|
|
@@ -72,14 +73,15 @@ const Dashboard = ({ projectsDir, onSelectSession, onSelectProject, error, onDis
|
|
|
72
73
|
const [sessionEntries, setSessionEntries] = useState([]);
|
|
73
74
|
const [baseSessionWorktrees, setBaseSessionWorktrees] = useState([]);
|
|
74
75
|
const [sessionRefreshKey, setSessionRefreshKey] = useState(0);
|
|
75
|
-
const { stdout } = useStdout();
|
|
76
|
-
const fixedRows = 6;
|
|
77
76
|
const displayError = error || loadError;
|
|
78
77
|
const { isSearchMode, searchQuery, selectedIndex, setSearchQuery } = useSearchMode(items.length, {
|
|
79
78
|
isDisabled: !!displayError,
|
|
80
79
|
skipInTest: false,
|
|
81
80
|
});
|
|
82
|
-
const limit =
|
|
81
|
+
const limit = useDynamicLimit({
|
|
82
|
+
isSearchMode,
|
|
83
|
+
hasError: !!displayError,
|
|
84
|
+
});
|
|
83
85
|
// Git status polling for session worktrees
|
|
84
86
|
const enrichedWorktrees = useGitStatus(baseSessionWorktrees, baseSessionWorktrees.length > 0 ? 'main' : null);
|
|
85
87
|
// Discover projects on mount
|
|
@@ -440,7 +442,7 @@ const Dashboard = ({ projectsDir, onSelectSession, onSelectProject, error, onDis
|
|
|
440
442
|
});
|
|
441
443
|
}
|
|
442
444
|
};
|
|
443
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsxs(Text, { bold: true, color: "green", children: ["CCManager - Dashboard v", version] }) }),
|
|
445
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsxs(Text, { bold: true, color: "green", children: ["CCManager - Dashboard v", version] }) }), loading ? (_jsx(Box, { children: _jsx(Text, { color: "yellow", children: "Discovering projects..." }) })) : projects.length === 0 && !displayError ? (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["No git repositories found in ", projectsDir] }) })) : (_jsx(SearchableList, { isSearchMode: isSearchMode, searchQuery: searchQuery, onSearchQueryChange: setSearchQuery, selectedIndex: selectedIndex, items: items, limit: limit, placeholder: "Type to filter...", noMatchMessage: "No matches found", children: _jsx(SelectInput, { items: items, onSelect: item => handleSelect(item), isFocused: !displayError, limit: limit, initialIndex: selectedIndex }) })), displayError && (_jsx(Box, { marginTop: 1, paddingX: 1, borderStyle: "round", borderColor: "red", children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["Error: ", displayError] }), _jsx(Text, { color: "gray", dimColor: true, children: "Press any key to dismiss" })] }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Status: ", STATUS_ICONS.BUSY, " ", STATUS_LABELS.BUSY, ' ', STATUS_ICONS.WAITING, " ", STATUS_LABELS.WAITING, " ", STATUS_ICONS.IDLE, ' ', STATUS_LABELS.IDLE] }), _jsx(Text, { dimColor: true, children: isSearchMode
|
|
444
446
|
? 'Search Mode: Type to filter, Enter to exit search, ESC to exit search'
|
|
445
447
|
: searchQuery
|
|
446
448
|
? `Filtered: "${searchQuery}" | ↑↓ Navigate Enter Select | /-Search ESC-Clear 0-9 Quick Select R-Refresh Q-Quit`
|
|
@@ -7,6 +7,10 @@ import { Effect } from 'effect';
|
|
|
7
7
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
8
8
|
import DeleteConfirmation from './DeleteConfirmation.js';
|
|
9
9
|
import { shortcutManager } from '../services/shortcutManager.js';
|
|
10
|
+
import { useSearchMode } from '../hooks/useSearchMode.js';
|
|
11
|
+
import { useDynamicLimit } from '../hooks/useDynamicLimit.js';
|
|
12
|
+
import { filterWorktreesByQuery } from '../utils/filterByQuery.js';
|
|
13
|
+
import SearchableList from './SearchableList.js';
|
|
10
14
|
const DeleteWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
11
15
|
const [worktrees, setWorktrees] = useState([]);
|
|
12
16
|
const [selectedIndices, setSelectedIndices] = useState(new Set());
|
|
@@ -14,6 +18,15 @@ const DeleteWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
|
14
18
|
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
15
19
|
const [error, setError] = useState(null);
|
|
16
20
|
const [isLoading, setIsLoading] = useState(true);
|
|
21
|
+
const [menuItems, setMenuItems] = useState([]);
|
|
22
|
+
// Use the search mode hook
|
|
23
|
+
const { isSearchMode, searchQuery, selectedIndex, setSearchQuery } = useSearchMode(menuItems.length, {
|
|
24
|
+
isDisabled: confirmMode,
|
|
25
|
+
});
|
|
26
|
+
const limit = useDynamicLimit({
|
|
27
|
+
isSearchMode,
|
|
28
|
+
hasError: !!error,
|
|
29
|
+
});
|
|
17
30
|
useEffect(() => {
|
|
18
31
|
let cancelled = false;
|
|
19
32
|
const loadWorktrees = async () => {
|
|
@@ -49,17 +62,22 @@ const DeleteWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
|
49
62
|
cancelled = true;
|
|
50
63
|
};
|
|
51
64
|
}, [projectPath]);
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
// Build menu items from worktrees, filtering by search query
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const filteredWorktrees = filterWorktreesByQuery(worktrees, searchQuery);
|
|
68
|
+
const items = filteredWorktrees.map(worktree => {
|
|
69
|
+
const originalIndex = worktrees.indexOf(worktree);
|
|
70
|
+
const branchName = worktree.branch
|
|
71
|
+
? worktree.branch.replace('refs/heads/', '')
|
|
72
|
+
: 'detached';
|
|
73
|
+
const isSelected = selectedIndices.has(originalIndex);
|
|
74
|
+
return {
|
|
75
|
+
label: `${isSelected ? '[✓]' : '[ ]'} ${branchName} (${worktree.path})`,
|
|
76
|
+
value: originalIndex.toString(),
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
setMenuItems(items);
|
|
80
|
+
}, [worktrees, searchQuery, selectedIndices]);
|
|
63
81
|
const handleSelect = (item) => {
|
|
64
82
|
// Don't toggle on Enter - this will be used to confirm
|
|
65
83
|
// We'll handle Space key separately for toggling
|
|
@@ -71,6 +89,10 @@ const DeleteWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
|
71
89
|
// Confirmation component handles input
|
|
72
90
|
return;
|
|
73
91
|
}
|
|
92
|
+
// Don't process other keys if in search mode (handled by useSearchMode)
|
|
93
|
+
if (isSearchMode) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
74
96
|
if (input === ' ') {
|
|
75
97
|
// Toggle selection on space
|
|
76
98
|
setSelectedIndices(prev => {
|
|
@@ -111,13 +133,14 @@ const DeleteWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
|
111
133
|
};
|
|
112
134
|
return (_jsx(DeleteConfirmation, { worktrees: selectedWorktrees, onConfirm: handleConfirm, onCancel: handleCancel }));
|
|
113
135
|
}
|
|
114
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "red", children: "Delete Worktrees" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Select worktrees to delete (Space to select, Enter to confirm):" }) }), _jsx(SelectInput, { items: menuItems, onSelect: handleSelect, onHighlight: (item) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "red", children: "Delete Worktrees" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Select worktrees to delete (Space to select, Enter to confirm):" }) }), _jsx(SearchableList, { isSearchMode: isSearchMode, searchQuery: searchQuery, onSearchQueryChange: setSearchQuery, selectedIndex: selectedIndex, items: menuItems, limit: limit, placeholder: "Type to filter worktrees...", noMatchMessage: "No worktrees match your search", children: _jsx(SelectInput, { items: menuItems, onSelect: handleSelect, onHighlight: (item) => {
|
|
137
|
+
const index = parseInt(item.value, 10);
|
|
138
|
+
setFocusedIndex(index);
|
|
139
|
+
}, limit: limit, indicatorComponent: ({ isSelected }) => (_jsx(Text, { color: isSelected ? 'green' : undefined, children: isSelected ? '>' : ' ' })), itemComponent: ({ isSelected, label }) => {
|
|
140
|
+
const hasCheckmark = label.includes('[✓]');
|
|
141
|
+
return (_jsx(Text, { color: isSelected ? 'green' : undefined, inverse: isSelected, dimColor: !isSelected && !hasCheckmark, children: label }));
|
|
142
|
+
} }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: isSearchMode
|
|
143
|
+
? 'Search Mode: Type to filter, Enter to exit search, ESC to exit search'
|
|
144
|
+
: `Controls: ↑↓/j/k Navigate, Space Select, Enter Confirm, /-Search, ${shortcutManager.getShortcutDisplay('cancel')} Cancel` }), selectedIndices.size > 0 && (_jsxs(Text, { color: "yellow", children: [selectedIndices.size, " worktree", selectedIndices.size > 1 ? 's' : '', ' ', "selected"] }))] })] }));
|
|
122
145
|
};
|
|
123
146
|
export default DeleteWorktree;
|
package/dist/components/Menu.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
|
-
import { Box, Text, useInput
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import SelectInput from 'ink-select-input';
|
|
5
5
|
import { Effect } from 'effect';
|
|
6
6
|
import { SessionManager } from '../services/sessionManager.js';
|
|
@@ -8,8 +8,10 @@ import { STATUS_ICONS, STATUS_LABELS, MENU_ICONS, } from '../constants/statusIco
|
|
|
8
8
|
import { useGitStatus } from '../hooks/useGitStatus.js';
|
|
9
9
|
import { prepareWorktreeItems, calculateColumnPositions, assembleWorktreeLabel, } from '../utils/worktreeUtils.js';
|
|
10
10
|
import { projectManager } from '../services/projectManager.js';
|
|
11
|
-
import TextInputWrapper from './TextInputWrapper.js';
|
|
12
11
|
import { useSearchMode } from '../hooks/useSearchMode.js';
|
|
12
|
+
import { useDynamicLimit } from '../hooks/useDynamicLimit.js';
|
|
13
|
+
import { filterWorktreesByQuery } from '../utils/filterByQuery.js';
|
|
14
|
+
import SearchableList from './SearchableList.js';
|
|
13
15
|
import { globalSessionOrchestrator } from '../services/globalSessionOrchestrator.js';
|
|
14
16
|
import { configReader } from '../services/config/configReader.js';
|
|
15
17
|
const createSeparatorWithText = (text, totalWidth = 35) => {
|
|
@@ -37,16 +39,14 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
|
|
|
37
39
|
const [recentProjects, setRecentProjects] = useState([]);
|
|
38
40
|
const [highlightedWorktreePath, setHighlightedWorktreePath] = useState(null);
|
|
39
41
|
const [autoApprovalToggleCounter, setAutoApprovalToggleCounter] = useState(0);
|
|
40
|
-
const { stdout } = useStdout();
|
|
41
|
-
const fixedRows = 6;
|
|
42
42
|
// Use the search mode hook
|
|
43
43
|
const { isSearchMode, searchQuery, selectedIndex, setSearchQuery } = useSearchMode(items.length, {
|
|
44
44
|
isDisabled: !!error || !!loadError,
|
|
45
45
|
});
|
|
46
|
-
const limit =
|
|
47
|
-
|
|
48
|
-
(
|
|
49
|
-
|
|
46
|
+
const limit = useDynamicLimit({
|
|
47
|
+
isSearchMode,
|
|
48
|
+
hasError: !!(error || loadError),
|
|
49
|
+
});
|
|
50
50
|
// Get worktree configuration for sorting
|
|
51
51
|
const worktreeConfig = configReader.getWorktreeConfig();
|
|
52
52
|
useEffect(() => {
|
|
@@ -129,14 +129,9 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
|
|
|
129
129
|
const items = prepareWorktreeItems(worktrees, sessions);
|
|
130
130
|
const columnPositions = calculateColumnPositions(items);
|
|
131
131
|
// Filter worktrees based on search query
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const searchLower = searchQuery.toLowerCase();
|
|
136
|
-
return (branchName.toLowerCase().includes(searchLower) ||
|
|
137
|
-
item.worktree.path.toLowerCase().includes(searchLower));
|
|
138
|
-
})
|
|
139
|
-
: items;
|
|
132
|
+
const filteredWorktrees = filterWorktreesByQuery(items.map(item => item.worktree), searchQuery);
|
|
133
|
+
const filteredWorktreeSet = new Set(filteredWorktrees);
|
|
134
|
+
const filteredItems = items.filter(item => filteredWorktreeSet.has(item.worktree));
|
|
140
135
|
// Build menu items with proper alignment
|
|
141
136
|
const menuItems = filteredItems.map((item, index) => {
|
|
142
137
|
const baseLabel = assembleWorktreeLabel(item, columnPositions);
|
|
@@ -510,19 +505,17 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
|
|
|
510
505
|
onSelectWorktree(item.worktree);
|
|
511
506
|
}
|
|
512
507
|
};
|
|
513
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "green", children: ["CCManager - Claude Code Worktree Manager v", version] }), projectName && (_jsx(Text, { bold: true, color: "green", children: projectName }))] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Select a worktree to start or resume a Claude Code session:" }) }),
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
}, isFocused: !error, initialIndex: selectedIndex, limit: limit })), (error || loadError) && (_jsx(Box, { marginTop: 1, paddingX: 1, borderStyle: "round", borderColor: "red", children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["Error: ", error || loadError] }), _jsx(Text, { color: "gray", dimColor: true, children: "Press any key to dismiss" })] }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Status: ", STATUS_ICONS.BUSY, " ", STATUS_LABELS.BUSY, ' ', STATUS_ICONS.WAITING, " ", STATUS_LABELS.WAITING, " ", STATUS_ICONS.IDLE, ' ', STATUS_LABELS.IDLE, configReader.isAutoApprovalEnabled() && (_jsxs(_Fragment, { children: [' | ', _jsx(Text, { color: "green", children: "Auto Approval Enabled" })] }))] }), _jsx(Text, { dimColor: true, children: isSearchMode
|
|
508
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "green", children: ["CCManager - Claude Code Worktree Manager v", version] }), projectName && (_jsx(Text, { bold: true, color: "green", children: projectName }))] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Select a worktree to start or resume a Claude Code session:" }) }), _jsx(SearchableList, { isSearchMode: isSearchMode, searchQuery: searchQuery, onSearchQueryChange: setSearchQuery, selectedIndex: selectedIndex, items: items, limit: limit, placeholder: "Type to filter worktrees...", noMatchMessage: "No worktrees match your search", children: _jsx(SelectInput, { items: items, onSelect: item => handleSelect(item), onHighlight: item => {
|
|
509
|
+
// ink-select-input may call onHighlight with undefined when items are empty
|
|
510
|
+
// (e.g., during menu re-mount after returning from a session), so guard it.
|
|
511
|
+
if (!item) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const menuItem = item;
|
|
515
|
+
if (menuItem.type === 'worktree') {
|
|
516
|
+
setHighlightedWorktreePath(menuItem.worktree.path);
|
|
517
|
+
}
|
|
518
|
+
}, isFocused: !error, initialIndex: selectedIndex, limit: limit }) }), (error || loadError) && (_jsx(Box, { marginTop: 1, paddingX: 1, borderStyle: "round", borderColor: "red", children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["Error: ", error || loadError] }), _jsx(Text, { color: "gray", dimColor: true, children: "Press any key to dismiss" })] }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Status: ", STATUS_ICONS.BUSY, " ", STATUS_LABELS.BUSY, ' ', STATUS_ICONS.WAITING, " ", STATUS_LABELS.WAITING, " ", STATUS_ICONS.IDLE, ' ', STATUS_LABELS.IDLE, configReader.isAutoApprovalEnabled() && (_jsxs(_Fragment, { children: [' | ', _jsx(Text, { color: "green", children: "Auto Approval Enabled" })] }))] }), _jsx(Text, { dimColor: true, children: isSearchMode
|
|
526
519
|
? 'Search Mode: Type to filter, Enter to exit search, ESC to exit search'
|
|
527
520
|
: searchQuery
|
|
528
521
|
? `Filtered: "${searchQuery}" | ↑↓ Navigate Enter Select | /-Search ESC-Clear 0-9 Quick Select N-New M-Merge D-Delete ${configReader.isAutoApprovalEnabled() ? 'A-AutoApproval ' : ''}${multiProject ? 'C-Config' : 'P-ProjConfig C-GlobalConfig'} ${projectName ? 'B-Back' : 'Q-Quit'}`
|
|
@@ -8,6 +8,7 @@ import { configReader } from '../services/config/configReader.js';
|
|
|
8
8
|
import { generateWorktreeDirectory } from '../utils/worktreeUtils.js';
|
|
9
9
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
10
10
|
import { useSearchMode } from '../hooks/useSearchMode.js';
|
|
11
|
+
import SearchableList from './SearchableList.js';
|
|
11
12
|
import { Effect } from 'effect';
|
|
12
13
|
import { describePromptInjection, getPromptInjectionMethod, } from '../utils/presetPrompt.js';
|
|
13
14
|
const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
@@ -238,7 +239,7 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
|
238
239
|
const promptMethod = selectedPreset
|
|
239
240
|
? getPromptInjectionMethod(selectedPreset)
|
|
240
241
|
: 'stdin';
|
|
241
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "green", children: "Create New Worktree" }) }), step === 'path' && !isAutoDirectory ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Enter worktree path (relative to repository root):" }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '> ' }), _jsx(TextInputWrapper, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })] })] })) : null, step === 'base-branch' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Select base branch for the worktree:" }) }),
|
|
242
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "green", children: "Create New Worktree" }) }), step === 'path' && !isAutoDirectory ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Enter worktree path (relative to repository root):" }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '> ' }), _jsx(TextInputWrapper, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })] })] })) : null, step === 'base-branch' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Select base branch for the worktree:" }) }), _jsx(SearchableList, { isSearchMode: isSearchMode, searchQuery: searchQuery, onSearchQueryChange: setSearchQuery, selectedIndex: selectedIndex, items: branchItems, limit: limit, placeholder: "Type to filter branches...", noMatchMessage: "No branches match your search", children: _jsx(SelectInput, { items: branchItems, onSelect: handleBaseBranchSelect, initialIndex: selectedIndex, limit: limit, isFocused: !isSearchMode }) }), !isSearchMode && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press / to search" }) }))] })), step === 'creation-mode' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Base branch: ", _jsx(Text, { color: "cyan", children: baseBranch })] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "How do you want to create the new worktree?" }) }), _jsx(SelectInput, { items: [
|
|
242
243
|
{
|
|
243
244
|
label: '1. Choose the branch name yourself',
|
|
244
245
|
value: 'manual',
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface ListItem {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string;
|
|
5
|
+
}
|
|
6
|
+
interface SearchableListProps {
|
|
7
|
+
isSearchMode: boolean;
|
|
8
|
+
searchQuery: string;
|
|
9
|
+
onSearchQueryChange: (query: string) => void;
|
|
10
|
+
selectedIndex: number;
|
|
11
|
+
items: ListItem[];
|
|
12
|
+
limit: number;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
noMatchMessage?: string;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Shared search mode UI: search input, filtered result list, and no-match message.
|
|
19
|
+
* Wraps a SelectInput (passed as children) which is shown when not in search mode.
|
|
20
|
+
*/
|
|
21
|
+
declare const SearchableList: React.FC<SearchableListProps>;
|
|
22
|
+
export default SearchableList;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import TextInputWrapper from './TextInputWrapper.js';
|
|
4
|
+
/**
|
|
5
|
+
* Shared search mode UI: search input, filtered result list, and no-match message.
|
|
6
|
+
* Wraps a SelectInput (passed as children) which is shown when not in search mode.
|
|
7
|
+
*/
|
|
8
|
+
const SearchableList = ({ isSearchMode, searchQuery, onSearchQueryChange, selectedIndex, items, limit, placeholder = 'Type to filter...', noMatchMessage = 'No matches found', children, }) => {
|
|
9
|
+
if (!isSearchMode) {
|
|
10
|
+
return _jsx(_Fragment, { children: children });
|
|
11
|
+
}
|
|
12
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { children: "Search: " }), _jsx(TextInputWrapper, { value: searchQuery, onChange: onSearchQueryChange, focus: true, placeholder: placeholder })] }), items.length === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "yellow", children: noMatchMessage }) })) : (_jsx(Box, { flexDirection: "column", children: items.slice(0, limit).map((item, index) => (_jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, children: [index === selectedIndex ? '❯ ' : ' ', item.label] }, item.value))) }))] }));
|
|
13
|
+
};
|
|
14
|
+
export default SearchableList;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface UseDynamicLimitOptions {
|
|
2
|
+
fixedRows?: number;
|
|
3
|
+
isSearchMode?: boolean;
|
|
4
|
+
hasError?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Calculate the maximum number of list items to display based on terminal height.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useDynamicLimit(options?: UseDynamicLimitOptions): number;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useStdout } from 'ink';
|
|
2
|
+
/**
|
|
3
|
+
* Calculate the maximum number of list items to display based on terminal height.
|
|
4
|
+
*/
|
|
5
|
+
export function useDynamicLimit(options = {}) {
|
|
6
|
+
const { fixedRows = 6, isSearchMode = false, hasError = false } = options;
|
|
7
|
+
const { stdout } = useStdout();
|
|
8
|
+
return Math.max(5, stdout.rows - fixedRows - (isSearchMode ? 1 : 0) - (hasError ? 3 : 0));
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter worktrees by matching search query against branch name and path.
|
|
3
|
+
*/
|
|
4
|
+
export function filterWorktreesByQuery(worktrees, query) {
|
|
5
|
+
if (!query)
|
|
6
|
+
return worktrees;
|
|
7
|
+
const searchLower = query.toLowerCase();
|
|
8
|
+
return worktrees.filter(worktree => {
|
|
9
|
+
const branchName = worktree.branch || '';
|
|
10
|
+
return (branchName.toLowerCase().includes(searchLower) ||
|
|
11
|
+
worktree.path.toLowerCase().includes(searchLower));
|
|
12
|
+
});
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "3.12.
|
|
3
|
+
"version": "3.12.1",
|
|
4
4
|
"description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kodai Kabasawa",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"bin"
|
|
42
42
|
],
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@kodaikabasawa/ccmanager-darwin-arm64": "3.12.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "3.12.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "3.12.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "3.12.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "3.12.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "3.12.1",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.12.1",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.12.1",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.12.1",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.12.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|