ccmanager 4.0.1 → 4.0.2

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.
@@ -105,7 +105,7 @@ const ConfigureStatusHooks = ({ onComplete, }) => {
105
105
  return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "green", children: "\u2713 Configuration saved successfully!" }) }));
106
106
  }
107
107
  if (view === 'edit') {
108
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Configure ", STATUS_LABELS[selectedStatus], " Hook"] }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Command to execute when status changes to", ' ', STATUS_LABELS[selectedStatus], ":"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(TextInputWrapper, { value: currentCommand, onChange: setCurrentCommand, onSubmit: handleCommandSubmit, placeholder: "Enter command (e.g., notify-send 'Claude is idle')" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Enabled: ", currentEnabled ? '✓' : '✗', " (Press Tab to toggle)"] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Environment variables available: CCMANAGER_OLD_STATE, CCMANAGER_NEW_STATE," }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `CCMANAGER_WORKTREE_PATH, CCMANAGER_WORKTREE_DIR, CCMANAGER_WORKTREE_BRANCH, CCMANAGER_SESSION_ID` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to save, Tab to toggle enabled, Esc to cancel" }) })] }));
108
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Configure ", STATUS_LABELS[selectedStatus], " Hook"] }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Command to execute when status changes to", ' ', STATUS_LABELS[selectedStatus], ":"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(TextInputWrapper, { value: currentCommand, onChange: setCurrentCommand, onSubmit: handleCommandSubmit, placeholder: "Enter command (e.g., notify-send 'Claude is idle')" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Enabled: ", currentEnabled ? '✓' : '✗', " (Press Tab to toggle)"] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Environment variables available: CCMANAGER_OLD_STATE, CCMANAGER_NEW_STATE," }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `CCMANAGER_WORKTREE_PATH, CCMANAGER_WORKTREE_DIR, CCMANAGER_WORKTREE_BRANCH, CCMANAGER_SESSION_ID, CCMANAGER_PRESET_NAME` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to save, Tab to toggle enabled, Esc to cancel" }) })] }));
109
109
  }
110
110
  const scopeLabel = scope === 'project' ? 'Project' : 'Global';
111
111
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Configure Status Hooks (", scopeLabel, ")"] }) }), isInheriting && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { backgroundColor: "cyan", color: "black", children: [' ', "\uD83D\uDCCB Inheriting from global configuration", ' '] }) })), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Set commands to run when session status changes:" }) }), _jsx(SelectInput, { items: getMenuItems(), onSelect: handleMenuSelect, isFocused: true, limit: 10 }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Esc to go back" }) })] }));
@@ -4,11 +4,6 @@ import { shortcutManager } from '../services/shortcutManager.js';
4
4
  const Session = ({ session, sessionManager, onReturnToMenu, }) => {
5
5
  const { stdout } = useStdout();
6
6
  const isExitingRef = useRef(false);
7
- const stripOscColorSequences = (input) => {
8
- // Remove default foreground/background color OSC sequences that Codex emits
9
- // These sequences leak as literal text when replaying buffered output
10
- return input.replace(/\x1B\](?:10|11);[^\x07\x1B]*(?:\x07|\x1B\\)/g, '');
11
- };
12
7
  const normalizeLineEndings = (input) => {
13
8
  // Ensure LF moves to column 0 to prevent cursor drift when ONLCR is disabled.
14
9
  let normalized = '';
@@ -32,22 +27,40 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
32
27
  // protocol / modifyOtherKeys / focus tracking) so they don't leak into other
33
28
  // sessions after we detach.
34
29
  stdout.write('\x1b[>0u'); // Disable kitty keyboard protocol (CSI u sequences)
35
- stdout.write('\x1b[>4m'); // Disable xterm modifyOtherKeys extensions
30
+ stdout.write('\x1b[>4;0m'); // Disable xterm modifyOtherKeys extensions
36
31
  stdout.write('\x1b[?1004l'); // Disable focus reporting
37
32
  stdout.write('\x1b[?2004l'); // Disable bracketed paste (can interfere with shortcuts)
38
33
  stdout.write('\x1b[?7h'); // Re-enable auto-wrap
39
34
  };
40
- const sanitizeReplayBuffer = (input) => {
41
- // Remove terminal mode toggles emitted by Codex so replay doesn't re-enable them
42
- // on our own TTY when restoring the session view.
43
- return stripOscColorSequences(input)
44
- .replace(/\x1B\[>4;?\d*m/g, '') // modifyOtherKeys set/reset
45
- .replace(/\x1B\[>[0-9;]*u/g, '') // kitty keyboard protocol enables
46
- .replace(/\x1B\[\?1004[hl]/g, '') // focus tracking
47
- .replace(/\x1B\[\?2004[hl]/g, ''); // bracketed paste
35
+ // Set up raw input handling
36
+ const stdin = process.stdin;
37
+ // Configure stdin for PTY passthrough
38
+ if (stdin.isTTY) {
39
+ stdin.setRawMode(true);
40
+ stdin.resume();
41
+ }
42
+ stdin.setEncoding('utf8');
43
+ const handleStdinData = (data) => {
44
+ if (isExitingRef.current)
45
+ return;
46
+ // Check for return to menu shortcut
47
+ if (shortcutManager.matchesRawInput('returnToMenu', data)) {
48
+ // Disable any extended input modes that might have been enabled by the PTY
49
+ if (stdout) {
50
+ resetTerminalInputModes();
51
+ }
52
+ // Remove our listener — Ink will reconfigure stdin when Menu mounts
53
+ stdin.removeListener('data', handleStdinData);
54
+ onReturnToMenu();
55
+ return;
56
+ }
57
+ if (session.stateMutex.getSnapshot().state === 'pending_auto_approval') {
58
+ sessionManager.cancelAutoApproval(session.id, 'User input received during auto-approval');
59
+ }
60
+ // Pass all other input directly to the PTY
61
+ session.process.write(data);
48
62
  };
49
- // Reset modes immediately on entry in case a previous session left them on
50
- resetTerminalInputModes();
63
+ stdin.on('data', handleStdinData);
51
64
  // Prevent line wrapping from drifting redraws in TUIs that rely on cursor-up clears.
52
65
  stdout.write('\x1b[?7l');
53
66
  // Clear screen when entering session
@@ -61,9 +74,8 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
61
74
  // Concatenate all history buffers and write at once for better performance
62
75
  const allHistory = Buffer.concat(restoredSession.outputHistory);
63
76
  const historyStr = allHistory.toString('utf8');
64
- // Sanitize and normalize the output
65
- const sanitized = sanitizeReplayBuffer(historyStr);
66
- const normalized = normalizeLineEndings(sanitized);
77
+ // Normalize the output
78
+ const normalized = normalizeLineEndings(historyStr);
67
79
  // Remove leading clear screen sequences to avoid double-clear
68
80
  const cleaned = normalized
69
81
  .replace(/^\x1B\[2J/g, '')
@@ -75,6 +87,21 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
75
87
  };
76
88
  // Listen for restore event first
77
89
  sessionManager.on('sessionRestore', handleSessionRestore);
90
+ // Listen for session data events
91
+ const handleSessionData = (activeSession, data) => {
92
+ // Only handle data for our session
93
+ if (activeSession.id === session.id && !isExitingRef.current) {
94
+ stdout.write(normalizeLineEndings(data));
95
+ }
96
+ };
97
+ const handleSessionExit = (exitedSession) => {
98
+ if (exitedSession.id === session.id) {
99
+ isExitingRef.current = true;
100
+ // Don't call onReturnToMenu here - App component handles it
101
+ }
102
+ };
103
+ sessionManager.on('sessionData', handleSessionData);
104
+ sessionManager.on('sessionExit', handleSessionExit);
78
105
  // Mark session as active (this will trigger the restore event)
79
106
  sessionManager.setSessionActive(session.id, true);
80
107
  // Immediately resize the PTY and terminal to current dimensions
@@ -93,21 +120,6 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
93
120
  catch {
94
121
  /* empty */
95
122
  }
96
- // Listen for session data events
97
- const handleSessionData = (activeSession, data) => {
98
- // Only handle data for our session
99
- if (activeSession.id === session.id && !isExitingRef.current) {
100
- stdout.write(normalizeLineEndings(data));
101
- }
102
- };
103
- const handleSessionExit = (exitedSession) => {
104
- if (exitedSession.id === session.id) {
105
- isExitingRef.current = true;
106
- // Don't call onReturnToMenu here - App component handles it
107
- }
108
- };
109
- sessionManager.on('sessionData', handleSessionData);
110
- sessionManager.on('sessionExit', handleSessionExit);
111
123
  // Handle terminal resize
112
124
  const handleResize = () => {
113
125
  const cols = process.stdout.columns || 80;
@@ -119,35 +131,6 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
119
131
  }
120
132
  };
121
133
  stdout.on('resize', handleResize);
122
- // Set up raw input handling
123
- const stdin = process.stdin;
124
- // Configure stdin for PTY passthrough
125
- if (stdin.isTTY) {
126
- stdin.setRawMode(true);
127
- stdin.resume();
128
- }
129
- stdin.setEncoding('utf8');
130
- const handleStdinData = (data) => {
131
- if (isExitingRef.current)
132
- return;
133
- // Check for return to menu shortcut
134
- if (shortcutManager.matchesRawInput('returnToMenu', data)) {
135
- // Disable any extended input modes that might have been enabled by the PTY
136
- if (stdout) {
137
- resetTerminalInputModes();
138
- }
139
- // Remove our listener — Ink will reconfigure stdin when Menu mounts
140
- stdin.removeListener('data', handleStdinData);
141
- onReturnToMenu();
142
- return;
143
- }
144
- if (session.stateMutex.getSnapshot().state === 'pending_auto_approval') {
145
- sessionManager.cancelAutoApproval(session.id, 'User input received during auto-approval');
146
- }
147
- // Pass all other input directly to the PTY
148
- session.process.write(data);
149
- };
150
- stdin.on('data', handleStdinData);
151
134
  return () => {
152
135
  // Remove our stdin listener
153
136
  stdin.removeListener('data', handleStdinData);
@@ -217,6 +217,7 @@ export class SessionManager extends EventEmitter {
217
217
  terminal,
218
218
  stateCheckInterval: undefined, // Will be set in setupBackgroundHandler
219
219
  isPrimaryCommand: options.isPrimaryCommand ?? true,
220
+ presetName: options.presetName,
220
221
  detectionStrategy,
221
222
  devcontainerConfig: options.devcontainerConfig ?? undefined,
222
223
  stateMutex: new Mutex(createInitialSessionStateData()),
@@ -257,6 +258,7 @@ export class SessionManager extends EventEmitter {
257
258
  const ptyProcess = await this.spawn(command, args, worktreePath);
258
259
  const session = await this.createSessionInternal(worktreePath, ptyProcess, {
259
260
  isPrimaryCommand: true,
261
+ presetName: preset.name,
260
262
  detectionStrategy: preset.detectionStrategy,
261
263
  });
262
264
  if (launch.stdinPayload) {
@@ -659,6 +661,7 @@ export class SessionManager extends EventEmitter {
659
661
  const ptyProcess = await this.spawn(devcontainerCmd, fullArgs, worktreePath);
660
662
  const session = await this.createSessionInternal(worktreePath, ptyProcess, {
661
663
  isPrimaryCommand: true,
664
+ presetName: preset.name,
662
665
  detectionStrategy: preset.detectionStrategy,
663
666
  devcontainerConfig,
664
667
  });
@@ -29,6 +29,7 @@ export interface Session {
29
29
  terminal: Terminal;
30
30
  stateCheckInterval: NodeJS.Timeout | undefined;
31
31
  isPrimaryCommand: boolean;
32
+ presetName: string | undefined;
32
33
  detectionStrategy: StateDetectionStrategy | undefined;
33
34
  devcontainerConfig: DevcontainerConfig | undefined;
34
35
  /**
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'child_process';
2
- import { dirname } from 'path';
2
+ import { basename } from 'path';
3
3
  import { Effect } from 'effect';
4
4
  import { ProcessError } from '../types/errors.js';
5
5
  import { WorktreeService } from '../services/worktreeService.js';
@@ -149,12 +149,13 @@ export function executeStatusHook(oldState, newState, session) {
149
149
  // Build environment for status hook
150
150
  const environment = {
151
151
  CCMANAGER_WORKTREE_PATH: session.worktreePath,
152
- CCMANAGER_WORKTREE_DIR: dirname(session.worktreePath),
152
+ CCMANAGER_WORKTREE_DIR: basename(session.worktreePath),
153
153
  CCMANAGER_WORKTREE_BRANCH: branch,
154
154
  CCMANAGER_GIT_ROOT: session.worktreePath, // For status hooks, we use worktree path as cwd
155
155
  CCMANAGER_OLD_STATE: oldState,
156
156
  CCMANAGER_NEW_STATE: newState,
157
157
  CCMANAGER_SESSION_ID: session.id,
158
+ CCMANAGER_PRESET_NAME: session.presetName || '',
158
159
  };
159
160
  yield* Effect.catchAll(executeHook(hook.command, session.worktreePath, environment), error => {
160
161
  // Log error but don't throw - hooks should not break the main flow
@@ -384,6 +384,7 @@ describe('hookExecutor Integration Tests', () => {
384
384
  outputHistory: [],
385
385
  stateCheckInterval: undefined,
386
386
  isPrimaryCommand: true,
387
+ presetName: undefined,
387
388
  detectionStrategy: 'claude',
388
389
  devcontainerConfig: undefined,
389
390
  lastActivity: new Date(),
@@ -440,6 +441,7 @@ describe('hookExecutor Integration Tests', () => {
440
441
  outputHistory: [],
441
442
  stateCheckInterval: undefined,
442
443
  isPrimaryCommand: true,
444
+ presetName: undefined,
443
445
  detectionStrategy: 'claude',
444
446
  devcontainerConfig: undefined,
445
447
  lastActivity: new Date(),
@@ -494,6 +496,7 @@ describe('hookExecutor Integration Tests', () => {
494
496
  outputHistory: [],
495
497
  stateCheckInterval: undefined,
496
498
  isPrimaryCommand: true,
499
+ presetName: undefined,
497
500
  detectionStrategy: 'claude',
498
501
  devcontainerConfig: undefined,
499
502
  lastActivity: new Date(),
@@ -550,6 +553,7 @@ describe('hookExecutor Integration Tests', () => {
550
553
  outputHistory: [],
551
554
  stateCheckInterval: undefined,
552
555
  isPrimaryCommand: true,
556
+ presetName: undefined,
553
557
  detectionStrategy: 'claude',
554
558
  devcontainerConfig: undefined,
555
559
  lastActivity: new Date(),
@@ -133,6 +133,7 @@ describe('prepareSessionItems', () => {
133
133
  terminal: {},
134
134
  stateCheckInterval: undefined,
135
135
  isPrimaryCommand: true,
136
+ presetName: undefined,
136
137
  detectionStrategy: 'claude',
137
138
  devcontainerConfig: undefined,
138
139
  stateMutex: new Mutex({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "4.0.1",
3
+ "version": "4.0.2",
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.0.1",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "4.0.1",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "4.0.1",
47
- "@kodaikabasawa/ccmanager-linux-x64": "4.0.1",
48
- "@kodaikabasawa/ccmanager-win32-x64": "4.0.1"
44
+ "@kodaikabasawa/ccmanager-darwin-arm64": "4.0.2",
45
+ "@kodaikabasawa/ccmanager-darwin-x64": "4.0.2",
46
+ "@kodaikabasawa/ccmanager-linux-arm64": "4.0.2",
47
+ "@kodaikabasawa/ccmanager-linux-x64": "4.0.2",
48
+ "@kodaikabasawa/ccmanager-win32-x64": "4.0.2"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.28.0",