ccmanager 4.1.7 → 4.1.8
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/ConfigureWorktree.js +9 -0
- package/dist/components/NewWorktree.js +38 -13
- package/dist/services/stateDetector/claude.js +1 -3
- package/dist/services/stateDetector/claude.test.js +21 -0
- package/dist/services/worktreeService.d.ts +11 -0
- package/dist/services/worktreeService.js +37 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +6 -6
|
@@ -16,6 +16,7 @@ const ConfigureWorktree = ({ onComplete }) => {
|
|
|
16
16
|
const [copySessionData, setCopySessionData] = useState(worktreeConfig.copySessionData ?? true);
|
|
17
17
|
const [sortByLastSession, setSortByLastSession] = useState(worktreeConfig.sortByLastSession ?? false);
|
|
18
18
|
const [autoUseDefaultBranch, setAutoUseDefaultBranch] = useState(worktreeConfig.autoUseDefaultBranch ?? false);
|
|
19
|
+
const [includeRemoteBranches, setIncludeRemoteBranches] = useState(worktreeConfig.includeRemoteBranches ?? false);
|
|
19
20
|
const [editMode, setEditMode] = useState('menu');
|
|
20
21
|
const [tempPattern, setTempPattern] = useState(pattern);
|
|
21
22
|
// Show if inheriting from global (for project scope)
|
|
@@ -50,6 +51,10 @@ const ConfigureWorktree = ({ onComplete }) => {
|
|
|
50
51
|
label: `Auto Use Default Branch: ${autoUseDefaultBranch ? '✅ Enabled' : '❌ Disabled'}`,
|
|
51
52
|
value: 'toggleAutoUseDefault',
|
|
52
53
|
},
|
|
54
|
+
{
|
|
55
|
+
label: `Include Remote Branches: ${includeRemoteBranches ? '✅ Enabled' : '❌ Disabled'}`,
|
|
56
|
+
value: 'toggleIncludeRemote',
|
|
57
|
+
},
|
|
53
58
|
{
|
|
54
59
|
label: '💾 Save Changes',
|
|
55
60
|
value: 'save',
|
|
@@ -77,6 +82,9 @@ const ConfigureWorktree = ({ onComplete }) => {
|
|
|
77
82
|
case 'toggleAutoUseDefault':
|
|
78
83
|
setAutoUseDefaultBranch(!autoUseDefaultBranch);
|
|
79
84
|
break;
|
|
85
|
+
case 'toggleIncludeRemote':
|
|
86
|
+
setIncludeRemoteBranches(!includeRemoteBranches);
|
|
87
|
+
break;
|
|
80
88
|
case 'save':
|
|
81
89
|
// Save the configuration
|
|
82
90
|
configEditor.setWorktreeConfig({
|
|
@@ -85,6 +93,7 @@ const ConfigureWorktree = ({ onComplete }) => {
|
|
|
85
93
|
copySessionData,
|
|
86
94
|
sortByLastSession,
|
|
87
95
|
autoUseDefaultBranch,
|
|
96
|
+
includeRemoteBranches,
|
|
88
97
|
});
|
|
89
98
|
onComplete();
|
|
90
99
|
break;
|
|
@@ -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 { useDynamicLimit } from '../hooks/useDynamicLimit.js';
|
|
11
12
|
import SearchableList from './SearchableList.js';
|
|
12
13
|
import { Effect } from 'effect';
|
|
13
14
|
import { describePromptInjection, getPromptInjectionMethod, } from '../utils/presetPrompt.js';
|
|
@@ -16,7 +17,7 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
|
16
17
|
const presetsConfig = configReader.getCommandPresets();
|
|
17
18
|
const isAutoDirectory = worktreeConfig.autoDirectory;
|
|
18
19
|
const isAutoUseDefaultBranch = worktreeConfig.autoUseDefaultBranch ?? false;
|
|
19
|
-
const
|
|
20
|
+
const includeRemoteBranches = worktreeConfig.includeRemoteBranches ?? false;
|
|
20
21
|
const getInitialStep = () => {
|
|
21
22
|
if (isAutoDirectory) {
|
|
22
23
|
return 'base-branch';
|
|
@@ -34,20 +35,28 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
|
34
35
|
const [isLoadingBranches, setIsLoadingBranches] = useState(true);
|
|
35
36
|
const [branchLoadError, setBranchLoadError] = useState(null);
|
|
36
37
|
const [branches, setBranches] = useState([]);
|
|
38
|
+
const [remoteBranches, setRemoteBranches] = useState([]);
|
|
37
39
|
const [defaultBranch, setDefaultBranch] = useState('main');
|
|
38
40
|
useEffect(() => {
|
|
39
41
|
let cancelled = false;
|
|
40
42
|
const service = new WorktreeService(projectPath);
|
|
41
43
|
const loadBranches = async () => {
|
|
42
|
-
const
|
|
44
|
+
const branchesEffect = includeRemoteBranches
|
|
45
|
+
? service.getBranchesWithRemotesEffect()
|
|
46
|
+
: Effect.map(service.getAllBranchesEffect(), (list) => ({
|
|
47
|
+
local: list,
|
|
48
|
+
remote: [],
|
|
49
|
+
}));
|
|
50
|
+
const workflow = Effect.all([branchesEffect, service.getDefaultBranchEffect()], { concurrency: 2 });
|
|
43
51
|
const result = await Effect.runPromise(Effect.match(workflow, {
|
|
44
52
|
onFailure: (error) => ({
|
|
45
53
|
type: 'error',
|
|
46
54
|
message: formatError(error),
|
|
47
55
|
}),
|
|
48
|
-
onSuccess: ([
|
|
56
|
+
onSuccess: ([branchData, defaultBr]) => ({
|
|
49
57
|
type: 'success',
|
|
50
|
-
|
|
58
|
+
local: branchData.local,
|
|
59
|
+
remote: branchData.remote,
|
|
51
60
|
defaultBranch: defaultBr,
|
|
52
61
|
}),
|
|
53
62
|
}));
|
|
@@ -57,7 +66,8 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
|
57
66
|
setIsLoadingBranches(false);
|
|
58
67
|
}
|
|
59
68
|
else {
|
|
60
|
-
setBranches(result.
|
|
69
|
+
setBranches(result.local);
|
|
70
|
+
setRemoteBranches(result.remote);
|
|
61
71
|
setDefaultBranch(result.defaultBranch);
|
|
62
72
|
setIsLoadingBranches(false);
|
|
63
73
|
if (isAutoUseDefaultBranch && result.defaultBranch) {
|
|
@@ -76,16 +86,31 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
|
76
86
|
return () => {
|
|
77
87
|
cancelled = true;
|
|
78
88
|
};
|
|
79
|
-
}, [projectPath, isAutoUseDefaultBranch]);
|
|
80
|
-
const allBranchItems = useMemo(() =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
}, [projectPath, isAutoUseDefaultBranch, includeRemoteBranches]);
|
|
90
|
+
const allBranchItems = useMemo(() => {
|
|
91
|
+
const defaultRemoteSuffix = `/${defaultBranch}`;
|
|
92
|
+
const defaultRemotes = remoteBranches.filter(br => br.endsWith(defaultRemoteSuffix));
|
|
93
|
+
const otherRemotes = remoteBranches.filter(br => !br.endsWith(defaultRemoteSuffix));
|
|
94
|
+
return [
|
|
95
|
+
{ label: `${defaultBranch} (default)`, value: defaultBranch },
|
|
96
|
+
...defaultRemotes.map(br => ({
|
|
97
|
+
label: `${br} (default remote)`,
|
|
98
|
+
value: br,
|
|
99
|
+
})),
|
|
100
|
+
...branches
|
|
101
|
+
.filter(br => br !== defaultBranch)
|
|
102
|
+
.map(br => ({ label: br, value: br })),
|
|
103
|
+
...otherRemotes.map(br => ({ label: br, value: br })),
|
|
104
|
+
];
|
|
105
|
+
}, [branches, remoteBranches, defaultBranch]);
|
|
86
106
|
const { isSearchMode, searchQuery, selectedIndex, setSearchQuery } = useSearchMode(allBranchItems.length, {
|
|
87
107
|
isDisabled: step !== 'base-branch',
|
|
88
108
|
});
|
|
109
|
+
const limit = useDynamicLimit({
|
|
110
|
+
fixedRows: includeRemoteBranches ? 10 : 8,
|
|
111
|
+
isSearchMode,
|
|
112
|
+
hasError: !!branchLoadError,
|
|
113
|
+
});
|
|
89
114
|
const branchItems = useMemo(() => {
|
|
90
115
|
if (!searchQuery)
|
|
91
116
|
return allBranchItems;
|
|
@@ -239,7 +264,7 @@ const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
|
|
|
239
264
|
const promptMethod = selectedPreset
|
|
240
265
|
? getPromptInjectionMethod(selectedPreset)
|
|
241
266
|
: 'stdin';
|
|
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" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: 'Tip: Enable "Auto Directory" in settings to generate paths automatically from branch names.' }) })] })) : 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: [
|
|
267
|
+
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" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: 'Tip: Enable "Auto Directory" in settings to generate paths automatically from branch names.' }) })] })) : 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" }) })), includeRemoteBranches && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tip: If the branch list feels slow, disable \"Include Remote Branches\" in Configuration \u2192 Configure Worktree Settings." }) }))] })), 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: [
|
|
243
268
|
{
|
|
244
269
|
label: '1. Choose the branch name yourself',
|
|
245
270
|
value: 'manual',
|
|
@@ -6,7 +6,6 @@ const SPINNER_CHARS = '✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀❁❂❃
|
|
|
6
6
|
const SPINNER_ACTIVITY_PATTERN = new RegExp(`^[${SPINNER_CHARS}] \\S+ing.*\u2026`, 'm');
|
|
7
7
|
// Session stats above the prompt, e.g. "(9m 21s · ↓ 13.7k tokens)" — requires parens, a digit, and "tokens"
|
|
8
8
|
const TOKEN_STATS_LINE_PATTERN = /\([^)]*\d[^)]*tokens\s*\)/i;
|
|
9
|
-
const BUSY_LOOKBACK_LINES = 5;
|
|
10
9
|
// Workaround: Claude Code sometimes appears idle in terminal output while
|
|
11
10
|
// still actively processing (busy). To mitigate false idle transitions,
|
|
12
11
|
// require terminal output to remain unchanged for this duration before
|
|
@@ -88,8 +87,7 @@ export class ClaudeStateDetector extends BaseStateDetector {
|
|
|
88
87
|
}
|
|
89
88
|
start--;
|
|
90
89
|
}
|
|
91
|
-
|
|
92
|
-
return recentBlock.slice(-BUSY_LOOKBACK_LINES).join('\n');
|
|
90
|
+
return lines.slice(Math.max(start, 0)).join('\n');
|
|
93
91
|
}
|
|
94
92
|
detectState(terminal, currentState) {
|
|
95
93
|
// Check for search prompt (⌕ Search…) within 200 lines - always idle (debounced)
|
|
@@ -446,6 +446,27 @@ describe('ClaudeStateDetector', () => {
|
|
|
446
446
|
// Assert - Should be idle because search prompt takes precedence
|
|
447
447
|
expect(state).toBe('idle');
|
|
448
448
|
});
|
|
449
|
+
it('should detect busy when spinner + token stats header is followed by a long TodoWrite checklist', () => {
|
|
450
|
+
// Regression: the recent block contains a spinner/token header and
|
|
451
|
+
// many checklist items with no internal blank line. All lines are
|
|
452
|
+
// part of the same contiguous update and should be inspected.
|
|
453
|
+
terminal = createMockTerminal([
|
|
454
|
+
'✽ Add GitHub Actions workflow and commit… (50s · ↓ 794 tokens)',
|
|
455
|
+
' ⎿ ✔ Create docs/index.config.json',
|
|
456
|
+
' ✔ Reorganize existing docs into topic directories',
|
|
457
|
+
' ✔ Add frontmatter to existing docs',
|
|
458
|
+
' ✔ Create docs/INDEX.md, docs/README.md, templates, workflow',
|
|
459
|
+
' ✔ Create root AGENTS.md and README.md',
|
|
460
|
+
' ✔ Write manifest generation Go script',
|
|
461
|
+
' ✔ Add Makefile tasks and run first generation',
|
|
462
|
+
' ◼ Add GitHub Actions workflow and commit',
|
|
463
|
+
'──────────────────────────────',
|
|
464
|
+
'❯',
|
|
465
|
+
'──────────────────────────────',
|
|
466
|
+
]);
|
|
467
|
+
const state = detector.detectState(terminal, 'idle');
|
|
468
|
+
expect(state).toBe('busy');
|
|
469
|
+
});
|
|
449
470
|
it('should ignore stale spinner output outside the latest block above the prompt box', () => {
|
|
450
471
|
terminal = createMockTerminal([
|
|
451
472
|
'✻ Seasoning… (44s · ↓ 247 tokens)',
|
|
@@ -201,6 +201,17 @@ export declare class WorktreeService {
|
|
|
201
201
|
* @throws {GitError} When git branch command fails (but falls back to empty array)
|
|
202
202
|
*/
|
|
203
203
|
getAllBranchesEffect(): Effect.Effect<string[], GitError, never>;
|
|
204
|
+
/**
|
|
205
|
+
* Effect-based getBranchesWithRemotes operation
|
|
206
|
+
* Returns local and remote branches separately so callers can distinguish them.
|
|
207
|
+
* Remote branches keep their `<remote>/<branch>` prefix (e.g. `origin/main`).
|
|
208
|
+
*
|
|
209
|
+
* @returns {Effect.Effect<{local: string[]; remote: string[]}, GitError, never>}
|
|
210
|
+
*/
|
|
211
|
+
getBranchesWithRemotesEffect(): Effect.Effect<{
|
|
212
|
+
local: string[];
|
|
213
|
+
remote: string[];
|
|
214
|
+
}, GitError, never>;
|
|
204
215
|
/**
|
|
205
216
|
* Effect-based getCurrentBranch operation
|
|
206
217
|
* Returns Effect that may fail with GitError
|
|
@@ -499,6 +499,43 @@ export class WorktreeService {
|
|
|
499
499
|
return Effect.succeed([]);
|
|
500
500
|
});
|
|
501
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* Effect-based getBranchesWithRemotes operation
|
|
504
|
+
* Returns local and remote branches separately so callers can distinguish them.
|
|
505
|
+
* Remote branches keep their `<remote>/<branch>` prefix (e.g. `origin/main`).
|
|
506
|
+
*
|
|
507
|
+
* @returns {Effect.Effect<{local: string[]; remote: string[]}, GitError, never>}
|
|
508
|
+
*/
|
|
509
|
+
getBranchesWithRemotesEffect() {
|
|
510
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
511
|
+
const self = this;
|
|
512
|
+
return Effect.catchAll(Effect.try({
|
|
513
|
+
try: () => {
|
|
514
|
+
const output = execSync("git branch -a --format='%(refname:short)' | grep -v HEAD | sort -u", {
|
|
515
|
+
cwd: self.rootPath,
|
|
516
|
+
encoding: 'utf8',
|
|
517
|
+
shell: '/bin/bash',
|
|
518
|
+
});
|
|
519
|
+
const remotes = self.getAllRemotes();
|
|
520
|
+
const remotePrefixes = remotes.map(r => `${r}/`);
|
|
521
|
+
const local = [];
|
|
522
|
+
const remote = [];
|
|
523
|
+
for (const raw of output.trim().split('\n')) {
|
|
524
|
+
const branch = raw.trim();
|
|
525
|
+
if (!branch)
|
|
526
|
+
continue;
|
|
527
|
+
if (remotePrefixes.some(prefix => branch.startsWith(prefix))) {
|
|
528
|
+
remote.push(branch);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
local.push(branch);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return { local, remote };
|
|
535
|
+
},
|
|
536
|
+
catch: (error) => error,
|
|
537
|
+
}), (_error) => Effect.succeed({ local: [], remote: [] }));
|
|
538
|
+
}
|
|
502
539
|
/**
|
|
503
540
|
* Effect-based getCurrentBranch operation
|
|
504
541
|
* Returns Effect that may fail with GitError
|
package/dist/types/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.8",
|
|
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": "4.1.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "4.1.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "4.1.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "4.1.8",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.8",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.8",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "4.1.8",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "4.1.8"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|