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.
@@ -102,8 +102,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
102
102
  : 'menu';
103
103
  navigateWithClear(targetView, () => {
104
104
  setMenuKey(prev => prev + 1);
105
- process.stdin.resume();
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
- // Ensure stdin is in a clean state for Ink components
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) => {
@@ -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 label = assembleWorktreeLabel(item, columnPositions);
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), 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] }), _jsx(Text, { dimColor: true, children: isSearchMode
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, useState } from 'react';
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 [isExiting, setIsExiting] = useState(false);
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 && !isExiting) {
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
- setIsExiting(true);
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.setRawMode(true);
129
- stdin.resume();
125
+ if (stdin.isTTY) {
126
+ stdin.setRawMode(true);
127
+ stdin.resume();
128
+ }
130
129
  stdin.setEncoding('utf8');
131
130
  const handleStdinData = (data) => {
132
- if (isExiting)
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
- // Restore stdin state before returning to menu
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 listener first to prevent any race conditions
152
+ // Remove our stdin listener
156
153
  stdin.removeListener('data', handleStdinData);
157
- // Disable focus reporting mode that might have been enabled by the PTY
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, isExiting]);
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.2",
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.2",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "3.7.2",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "3.7.2",
47
- "@kodaikabasawa/ccmanager-linux-x64": "3.7.2",
48
- "@kodaikabasawa/ccmanager-win32-x64": "3.7.2"
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",