ccmanager 3.7.2 → 3.7.4
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/App.js +2 -10
- package/dist/components/Menu.js +34 -5
- package/dist/components/Menu.recent-projects.test.js +2 -0
- package/dist/components/Session.js +13 -26
- package/dist/services/sessionManager.d.ts +3 -0
- package/dist/services/sessionManager.js +17 -1
- package/package.json +6 -6
package/dist/components/App.js
CHANGED
|
@@ -102,8 +102,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
102
102
|
: 'menu';
|
|
103
103
|
navigateWithClear(targetView, () => {
|
|
104
104
|
setMenuKey(prev => prev + 1);
|
|
105
|
-
|
|
106
|
-
process.stdin.setEncoding('utf8');
|
|
105
|
+
// Ink's useInput in Menu will reconfigure stdin automatically
|
|
107
106
|
});
|
|
108
107
|
}
|
|
109
108
|
return current;
|
|
@@ -265,14 +264,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
265
264
|
: 'menu';
|
|
266
265
|
navigateWithClear(targetView, () => {
|
|
267
266
|
setMenuKey(prev => prev + 1); // Force menu refresh
|
|
268
|
-
//
|
|
269
|
-
if (process.stdin.isTTY) {
|
|
270
|
-
// Flush any pending input to prevent escape sequences from leaking
|
|
271
|
-
process.stdin.read();
|
|
272
|
-
process.stdin.setRawMode(false);
|
|
273
|
-
process.stdin.resume();
|
|
274
|
-
process.stdin.setEncoding('utf8');
|
|
275
|
-
}
|
|
267
|
+
// Ink's useInput in Menu will reconfigure stdin automatically
|
|
276
268
|
});
|
|
277
269
|
};
|
|
278
270
|
const handleCreateWorktree = async (path, branch, baseBranch, copySessionData, copyClaudeDirectory) => {
|
package/dist/components/Menu.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
4
|
import SelectInput from 'ink-select-input';
|
|
@@ -35,6 +35,8 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
|
|
|
35
35
|
const [sessions, setSessions] = useState([]);
|
|
36
36
|
const [items, setItems] = useState([]);
|
|
37
37
|
const [recentProjects, setRecentProjects] = useState([]);
|
|
38
|
+
const [highlightedWorktreePath, setHighlightedWorktreePath] = useState(null);
|
|
39
|
+
const [autoApprovalToggleCounter, setAutoApprovalToggleCounter] = useState(0);
|
|
38
40
|
const { stdout } = useStdout();
|
|
39
41
|
const fixedRows = 6;
|
|
40
42
|
// Use the search mode hook
|
|
@@ -137,7 +139,10 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
|
|
|
137
139
|
: items;
|
|
138
140
|
// Build menu items with proper alignment
|
|
139
141
|
const menuItems = filteredItems.map((item, index) => {
|
|
140
|
-
const
|
|
142
|
+
const baseLabel = assembleWorktreeLabel(item, columnPositions);
|
|
143
|
+
const aaDisabled = configReader.isAutoApprovalEnabled() &&
|
|
144
|
+
sessionManager.isAutoApprovalDisabledForWorktree(item.worktree.path);
|
|
145
|
+
const label = baseLabel + (aaDisabled ? ' [Auto Approval Off]' : '');
|
|
141
146
|
// Only show numbers for worktrees (0-9) when not in search mode
|
|
142
147
|
const numberPrefix = !isSearchMode && index < 10 ? `${index} ❯ ` : '❯ ';
|
|
143
148
|
return {
|
|
@@ -255,6 +260,16 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
|
|
|
255
260
|
}
|
|
256
261
|
}
|
|
257
262
|
setItems(menuItems);
|
|
263
|
+
// Ensure highlighted worktree path is valid for hotkey support
|
|
264
|
+
// (e.g., on initial render or when returning from a session view)
|
|
265
|
+
setHighlightedWorktreePath(prev => {
|
|
266
|
+
if (prev &&
|
|
267
|
+
menuItems.some(item => item.type === 'worktree' && item.value === prev)) {
|
|
268
|
+
return prev;
|
|
269
|
+
}
|
|
270
|
+
const first = menuItems.find(item => item.type === 'worktree');
|
|
271
|
+
return first && first.type === 'worktree' ? first.worktree.path : null;
|
|
272
|
+
});
|
|
258
273
|
}, [
|
|
259
274
|
worktrees,
|
|
260
275
|
sessions,
|
|
@@ -264,6 +279,8 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
|
|
|
264
279
|
recentProjects,
|
|
265
280
|
searchQuery,
|
|
266
281
|
isSearchMode,
|
|
282
|
+
autoApprovalToggleCounter,
|
|
283
|
+
sessionManager,
|
|
267
284
|
]);
|
|
268
285
|
// Handle hotkeys
|
|
269
286
|
useInput((input, _key) => {
|
|
@@ -309,6 +326,13 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
|
|
|
309
326
|
return;
|
|
310
327
|
}
|
|
311
328
|
switch (keyPressed) {
|
|
329
|
+
case 'a':
|
|
330
|
+
// Toggle auto-approval for the currently highlighted worktree
|
|
331
|
+
if (configReader.isAutoApprovalEnabled() && highlightedWorktreePath) {
|
|
332
|
+
sessionManager.toggleAutoApprovalForWorktree(highlightedWorktreePath);
|
|
333
|
+
setAutoApprovalToggleCounter(c => c + 1);
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
312
336
|
case 'n':
|
|
313
337
|
// Trigger new worktree action
|
|
314
338
|
onSelectWorktree({
|
|
@@ -487,10 +511,15 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
|
|
|
487
511
|
};
|
|
488
512
|
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:" }) }), isSearchMode && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { children: "Search: " }), _jsx(TextInputWrapper, { value: searchQuery, onChange: setSearchQuery, focus: true, placeholder: "Type to filter worktrees..." })] })), isSearchMode && items.length === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "yellow", children: "No worktrees match your search" }) })) : isSearchMode ? (
|
|
489
513
|
// In search mode, show the items as a list without SelectInput
|
|
490
|
-
_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))) })) : (_jsx(SelectInput, { items: items, onSelect: item => handleSelect(item),
|
|
514
|
+
_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))) })) : (_jsx(SelectInput, { items: items, onSelect: item => handleSelect(item), onHighlight: item => {
|
|
515
|
+
const menuItem = item;
|
|
516
|
+
if (menuItem.type === 'worktree') {
|
|
517
|
+
setHighlightedWorktreePath(menuItem.worktree.path);
|
|
518
|
+
}
|
|
519
|
+
}, 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
|
|
491
520
|
? 'Search Mode: Type to filter, Enter to exit search, ESC to exit search'
|
|
492
521
|
: searchQuery
|
|
493
|
-
? `Filtered: "${searchQuery}" | ↑↓ Navigate Enter Select | /-Search ESC-Clear 0-9 Quick Select N-New M-Merge D-Delete ${multiProject ? 'C-Config' : 'P-ProjConfig C-GlobalConfig'} ${projectName ? 'B-Back' : 'Q-Quit'}`
|
|
494
|
-
: `Controls: ↑↓ Navigate Enter Select | Hotkeys: 0-9 Quick Select /-Search N-New M-Merge D-Delete ${multiProject ? 'C-Config' : 'P-ProjConfig C-GlobalConfig'} ${projectName ? 'B-Back' : 'Q-Quit'}` })] })] }));
|
|
522
|
+
? `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'}`
|
|
523
|
+
: `Controls: ↑↓ Navigate Enter Select | Hotkeys: 0-9 Quick Select /-Search N-New M-Merge D-Delete ${configReader.isAutoApprovalEnabled() ? 'A-AutoApproval ' : ''}${multiProject ? 'C-Config' : 'P-ProjConfig C-GlobalConfig'} ${projectName ? 'B-Back' : 'Q-Quit'}` })] })] }));
|
|
495
524
|
};
|
|
496
525
|
export default Menu;
|
|
@@ -84,6 +84,8 @@ describe('Menu - Recent Projects', () => {
|
|
|
84
84
|
createSessionWithPreset: vi.fn(),
|
|
85
85
|
createSessionWithDevcontainer: vi.fn(),
|
|
86
86
|
destroy: vi.fn(),
|
|
87
|
+
isAutoApprovalDisabledForWorktree: vi.fn().mockReturnValue(false),
|
|
88
|
+
toggleAutoApprovalForWorktree: vi.fn().mockReturnValue(false),
|
|
87
89
|
};
|
|
88
90
|
mockWorktreeService = {
|
|
89
91
|
getWorktreesEffect: vi.fn().mockReturnValue(Effect.succeed([
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { useStdout } from 'ink';
|
|
3
3
|
import { shortcutManager } from '../services/shortcutManager.js';
|
|
4
4
|
const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
5
5
|
const { stdout } = useStdout();
|
|
6
|
-
const
|
|
6
|
+
const isExitingRef = useRef(false);
|
|
7
7
|
const stripOscColorSequences = (input) => {
|
|
8
8
|
// Remove default foreground/background color OSC sequences that Codex emits
|
|
9
9
|
// These sequences leak as literal text when replaying buffered output
|
|
@@ -96,13 +96,13 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
96
96
|
// Listen for session data events
|
|
97
97
|
const handleSessionData = (activeSession, data) => {
|
|
98
98
|
// Only handle data for our session
|
|
99
|
-
if (activeSession.id === session.id && !
|
|
99
|
+
if (activeSession.id === session.id && !isExitingRef.current) {
|
|
100
100
|
stdout.write(normalizeLineEndings(data));
|
|
101
101
|
}
|
|
102
102
|
};
|
|
103
103
|
const handleSessionExit = (exitedSession) => {
|
|
104
104
|
if (exitedSession.id === session.id) {
|
|
105
|
-
|
|
105
|
+
isExitingRef.current = true;
|
|
106
106
|
// Don't call onReturnToMenu here - App component handles it
|
|
107
107
|
}
|
|
108
108
|
};
|
|
@@ -121,15 +121,14 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
121
121
|
stdout.on('resize', handleResize);
|
|
122
122
|
// Set up raw input handling
|
|
123
123
|
const stdin = process.stdin;
|
|
124
|
-
// Store original stdin state
|
|
125
|
-
const originalIsRaw = stdin.isRaw;
|
|
126
|
-
const originalIsPaused = stdin.isPaused();
|
|
127
124
|
// Configure stdin for PTY passthrough
|
|
128
|
-
stdin.
|
|
129
|
-
|
|
125
|
+
if (stdin.isTTY) {
|
|
126
|
+
stdin.setRawMode(true);
|
|
127
|
+
stdin.resume();
|
|
128
|
+
}
|
|
130
129
|
stdin.setEncoding('utf8');
|
|
131
130
|
const handleStdinData = (data) => {
|
|
132
|
-
if (
|
|
131
|
+
if (isExitingRef.current)
|
|
133
132
|
return;
|
|
134
133
|
// Check for return to menu shortcut
|
|
135
134
|
if (shortcutManager.matchesRawInput('returnToMenu', data)) {
|
|
@@ -137,10 +136,8 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
137
136
|
if (stdout) {
|
|
138
137
|
resetTerminalInputModes();
|
|
139
138
|
}
|
|
140
|
-
//
|
|
139
|
+
// Remove our listener — Ink will reconfigure stdin when Menu mounts
|
|
141
140
|
stdin.removeListener('data', handleStdinData);
|
|
142
|
-
stdin.setRawMode(false);
|
|
143
|
-
stdin.pause();
|
|
144
141
|
onReturnToMenu();
|
|
145
142
|
return;
|
|
146
143
|
}
|
|
@@ -152,22 +149,12 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
152
149
|
};
|
|
153
150
|
stdin.on('data', handleStdinData);
|
|
154
151
|
return () => {
|
|
155
|
-
// Remove
|
|
152
|
+
// Remove our stdin listener
|
|
156
153
|
stdin.removeListener('data', handleStdinData);
|
|
157
|
-
// Disable
|
|
154
|
+
// Disable extended input modes that might have been enabled by the PTY
|
|
158
155
|
if (stdout) {
|
|
159
156
|
resetTerminalInputModes();
|
|
160
157
|
}
|
|
161
|
-
// Restore stdin to its original state
|
|
162
|
-
if (stdin.isTTY) {
|
|
163
|
-
stdin.setRawMode(originalIsRaw || false);
|
|
164
|
-
if (originalIsPaused) {
|
|
165
|
-
stdin.pause();
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
stdin.resume();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
158
|
// Mark session as inactive
|
|
172
159
|
sessionManager.setSessionActive(session.worktreePath, false);
|
|
173
160
|
// Remove event listeners
|
|
@@ -176,7 +163,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
176
163
|
sessionManager.off('sessionExit', handleSessionExit);
|
|
177
164
|
stdout.off('resize', handleResize);
|
|
178
165
|
};
|
|
179
|
-
}, [session, sessionManager, stdout, onReturnToMenu
|
|
166
|
+
}, [session, sessionManager, stdout, onReturnToMenu]);
|
|
180
167
|
return null;
|
|
181
168
|
};
|
|
182
169
|
export default Session;
|
|
@@ -14,6 +14,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
|
|
|
14
14
|
sessions: Map<string, Session>;
|
|
15
15
|
private waitingWithBottomBorder;
|
|
16
16
|
private busyTimers;
|
|
17
|
+
private autoApprovalDisabledWorktrees;
|
|
17
18
|
private spawn;
|
|
18
19
|
detectTerminalState(session: Session): SessionState;
|
|
19
20
|
detectBackgroundTask(session: Session): number;
|
|
@@ -66,6 +67,8 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
|
|
|
66
67
|
getSession(worktreePath: string): Session | undefined;
|
|
67
68
|
setSessionActive(worktreePath: string, active: boolean): void;
|
|
68
69
|
cancelAutoApproval(worktreePath: string, reason?: string): void;
|
|
70
|
+
toggleAutoApprovalForWorktree(worktreePath: string): boolean;
|
|
71
|
+
isAutoApprovalDisabledForWorktree(worktreePath: string): boolean;
|
|
69
72
|
destroySession(worktreePath: string): void;
|
|
70
73
|
/**
|
|
71
74
|
* Terminate session and cleanup resources using Effect-based error handling
|
|
@@ -22,6 +22,7 @@ export class SessionManager extends EventEmitter {
|
|
|
22
22
|
sessions;
|
|
23
23
|
waitingWithBottomBorder = new Map();
|
|
24
24
|
busyTimers = new Map();
|
|
25
|
+
autoApprovalDisabledWorktrees = new Set();
|
|
25
26
|
async spawn(command, args, worktreePath) {
|
|
26
27
|
const spawnOptions = {
|
|
27
28
|
name: 'xterm-256color',
|
|
@@ -38,7 +39,8 @@ export class SessionManager extends EventEmitter {
|
|
|
38
39
|
// If auto-approval is enabled and state is waiting_input, convert to pending_auto_approval
|
|
39
40
|
if (detectedState === 'waiting_input' &&
|
|
40
41
|
configReader.isAutoApprovalEnabled() &&
|
|
41
|
-
!stateData.autoApprovalFailed
|
|
42
|
+
!stateData.autoApprovalFailed &&
|
|
43
|
+
!this.autoApprovalDisabledWorktrees.has(session.worktreePath)) {
|
|
42
44
|
return 'pending_auto_approval';
|
|
43
45
|
}
|
|
44
46
|
return detectedState;
|
|
@@ -483,6 +485,20 @@ export class SessionManager extends EventEmitter {
|
|
|
483
485
|
}));
|
|
484
486
|
}
|
|
485
487
|
}
|
|
488
|
+
toggleAutoApprovalForWorktree(worktreePath) {
|
|
489
|
+
if (this.autoApprovalDisabledWorktrees.has(worktreePath)) {
|
|
490
|
+
this.autoApprovalDisabledWorktrees.delete(worktreePath);
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
this.autoApprovalDisabledWorktrees.add(worktreePath);
|
|
495
|
+
this.cancelAutoApproval(worktreePath, 'Auto-approval disabled for worktree');
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
isAutoApprovalDisabledForWorktree(worktreePath) {
|
|
500
|
+
return this.autoApprovalDisabledWorktrees.has(worktreePath);
|
|
501
|
+
}
|
|
486
502
|
destroySession(worktreePath) {
|
|
487
503
|
const session = this.sessions.get(worktreePath);
|
|
488
504
|
if (session) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.4",
|
|
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.7.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "3.7.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "3.7.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "3.7.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "3.7.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "3.7.4",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.7.4",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.7.4",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.7.4",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.7.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|