ccmanager 3.7.4 → 3.8.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.
Files changed (38) hide show
  1. package/README.md +10 -0
  2. package/dist/components/Menu.js +5 -0
  3. package/dist/components/Menu.recent-projects.test.js +2 -0
  4. package/dist/components/Menu.test.js +2 -0
  5. package/dist/constants/statusIcons.d.ts +2 -1
  6. package/dist/constants/statusIcons.js +9 -2
  7. package/dist/constants/statusIcons.test.js +33 -1
  8. package/dist/services/sessionManager.autoApproval.test.js +1 -0
  9. package/dist/services/sessionManager.d.ts +2 -0
  10. package/dist/services/sessionManager.js +30 -11
  11. package/dist/services/sessionManager.test.js +131 -23
  12. package/dist/services/stateDetector/base.d.ts +1 -0
  13. package/dist/services/stateDetector/claude.d.ts +1 -0
  14. package/dist/services/stateDetector/claude.js +14 -0
  15. package/dist/services/stateDetector/claude.test.js +64 -0
  16. package/dist/services/stateDetector/cline.d.ts +1 -0
  17. package/dist/services/stateDetector/cline.js +3 -0
  18. package/dist/services/stateDetector/codex.d.ts +1 -0
  19. package/dist/services/stateDetector/codex.js +3 -0
  20. package/dist/services/stateDetector/cursor.d.ts +1 -0
  21. package/dist/services/stateDetector/cursor.js +3 -0
  22. package/dist/services/stateDetector/gemini.d.ts +1 -0
  23. package/dist/services/stateDetector/gemini.js +3 -0
  24. package/dist/services/stateDetector/github-copilot.d.ts +1 -0
  25. package/dist/services/stateDetector/github-copilot.js +3 -0
  26. package/dist/services/stateDetector/kimi.d.ts +1 -0
  27. package/dist/services/stateDetector/kimi.js +3 -0
  28. package/dist/services/stateDetector/opencode.d.ts +1 -0
  29. package/dist/services/stateDetector/opencode.js +3 -0
  30. package/dist/services/stateDetector/types.d.ts +1 -0
  31. package/dist/utils/commandArgs.d.ts +9 -0
  32. package/dist/utils/commandArgs.js +15 -0
  33. package/dist/utils/commandArgs.test.d.ts +1 -0
  34. package/dist/utils/commandArgs.test.js +52 -0
  35. package/dist/utils/mutex.d.ts +1 -0
  36. package/dist/utils/mutex.js +1 -0
  37. package/dist/utils/worktreeUtils.js +1 -1
  38. package/package.json +6 -6
package/README.md CHANGED
@@ -162,6 +162,16 @@ CCManager supports configuring the command and arguments used to run Claude Code
162
162
 
163
163
  For detailed configuration options and examples, see [docs/command-config.md](docs/command-config.md).
164
164
 
165
+ ## Claude Code Teammate Mode
166
+
167
+ When running the `claude` command with the default (`claude`) detection strategy, CCManager automatically appends `--teammate-mode in-process` to the CLI arguments. This prevents conflicts between Claude Code's agent teams feature and ccmanager's PTY-based session management.
168
+
169
+ - **Automatic**: No configuration needed. CCManager injects the flag for all `claude` command sessions.
170
+ - **Override**: If you explicitly specify `--teammate-mode` in your preset args, your value takes priority.
171
+ - **Non-claude commands**: Other AI assistants (Gemini, Codex, etc.) are not affected.
172
+
173
+ Setting `"teammateMode": "in-process"` in Claude Code's `settings.json` alone is not sufficient when running inside a tmux-like environment, which is why CCManager controls this via the CLI argument.
174
+
165
175
 
166
176
  ## Session Data Copying
167
177
 
@@ -512,6 +512,11 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
512
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 ? (
513
513
  // In search mode, show the items as a list without SelectInput
514
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
+ // ink-select-input may call onHighlight with undefined when items are empty
516
+ // (e.g., during menu re-mount after returning from a session), so guard it.
517
+ if (!item) {
518
+ return;
519
+ }
515
520
  const menuItem = item;
516
521
  if (menuItem.type === 'worktree') {
517
522
  setHighlightedWorktreePath(menuItem.worktree.path);
@@ -126,6 +126,7 @@ describe('Menu - Recent Projects', () => {
126
126
  pending_auto_approval: 0,
127
127
  total: 0,
128
128
  backgroundTasks: 0,
129
+ teamMembers: 0,
129
130
  });
130
131
  vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
131
132
  const { lastFrame, rerender } = render(_jsx(Menu, { sessionManager: mockSessionManager, worktreeService: mockWorktreeService, onSelectWorktree: vi.fn(), onSelectRecentProject: vi.fn(), multiProject: true, version: "test" }));
@@ -159,6 +160,7 @@ describe('Menu - Recent Projects', () => {
159
160
  pending_auto_approval: 0,
160
161
  total: 0,
161
162
  backgroundTasks: 0,
163
+ teamMembers: 0,
162
164
  });
163
165
  vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
164
166
  const { lastFrame } = render(_jsx(Menu, { sessionManager: mockSessionManager, worktreeService: mockWorktreeService, onSelectWorktree: vi.fn(), onSelectRecentProject: vi.fn(), multiProject: true, version: "test" }));
@@ -277,6 +277,7 @@ describe('Menu component rendering', () => {
277
277
  pending_auto_approval: 0,
278
278
  total: 0,
279
279
  backgroundTasks: 0,
280
+ teamMembers: 0,
280
281
  });
281
282
  vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
282
283
  const { lastFrame } = render(_jsx(Menu, { sessionManager: sessionManager, worktreeService: worktreeService, onSelectWorktree: onSelectWorktree, onSelectRecentProject: onSelectRecentProject, multiProject: true, version: "test" }));
@@ -319,6 +320,7 @@ describe('Menu component rendering', () => {
319
320
  pending_auto_approval: 0,
320
321
  total: 0,
321
322
  backgroundTasks: 0,
323
+ teamMembers: 0,
322
324
  });
323
325
  vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
324
326
  const { lastFrame } = render(_jsx(Menu, { sessionManager: sessionManager, worktreeService: worktreeService, onSelectWorktree: onSelectWorktree, onSelectRecentProject: onSelectRecentProject, multiProject: true, version: "test" }));
@@ -14,6 +14,7 @@ export declare const STATUS_TAGS: {
14
14
  readonly BACKGROUND_TASK: "\u001B[2m[BG]\u001B[0m";
15
15
  };
16
16
  export declare const getBackgroundTaskTag: (count: number) => string;
17
+ export declare const getTeamMemberTag: (count: number) => string;
17
18
  export declare const MENU_ICONS: {
18
19
  readonly NEW_WORKTREE: "⊕";
19
20
  readonly MERGE_WORKTREE: "⇄";
@@ -21,4 +22,4 @@ export declare const MENU_ICONS: {
21
22
  readonly CONFIGURE_SHORTCUTS: "⌨";
22
23
  readonly EXIT: "⏻";
23
24
  };
24
- export declare const getStatusDisplay: (status: SessionState, backgroundTaskCount?: number) => string;
25
+ export declare const getStatusDisplay: (status: SessionState, backgroundTaskCount?: number, teamMemberCount?: number) => string;
@@ -22,6 +22,11 @@ export const getBackgroundTaskTag = (count) => {
22
22
  // count >= 2: show [BG:N]
23
23
  return `\x1b[2m[BG:${count}]\x1b[0m`;
24
24
  };
25
+ export const getTeamMemberTag = (count) => {
26
+ if (count <= 0)
27
+ return '';
28
+ return `\x1b[2m[Team:${count}]\x1b[0m`;
29
+ };
25
30
  export const MENU_ICONS = {
26
31
  NEW_WORKTREE: '⊕',
27
32
  MERGE_WORKTREE: '⇄',
@@ -41,8 +46,10 @@ const getBaseStatusDisplay = (status) => {
41
46
  return `${STATUS_ICONS.IDLE} ${STATUS_LABELS.IDLE}`;
42
47
  }
43
48
  };
44
- export const getStatusDisplay = (status, backgroundTaskCount = 0) => {
49
+ export const getStatusDisplay = (status, backgroundTaskCount = 0, teamMemberCount = 0) => {
45
50
  const display = getBaseStatusDisplay(status);
46
51
  const bgTag = getBackgroundTaskTag(backgroundTaskCount);
47
- return bgTag ? `${display} ${bgTag}` : display;
52
+ const teamTag = getTeamMemberTag(teamMemberCount);
53
+ const suffix = [bgTag, teamTag].filter(Boolean).join(' ');
54
+ return suffix ? `${display} ${suffix}` : display;
48
55
  };
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { getStatusDisplay, getBackgroundTaskTag, STATUS_ICONS, STATUS_LABELS, STATUS_TAGS, } from './statusIcons.js';
2
+ import { getStatusDisplay, getBackgroundTaskTag, getTeamMemberTag, STATUS_ICONS, STATUS_LABELS, STATUS_TAGS, } from './statusIcons.js';
3
3
  describe('getStatusDisplay', () => {
4
4
  it('should return busy display for busy state', () => {
5
5
  const result = getStatusDisplay('busy');
@@ -70,3 +70,35 @@ describe('getBackgroundTaskTag', () => {
70
70
  expect(result).toBe('\x1b[2m[BG:10]\x1b[0m');
71
71
  });
72
72
  });
73
+ describe('getTeamMemberTag', () => {
74
+ it('should return empty string when count is 0', () => {
75
+ expect(getTeamMemberTag(0)).toBe('');
76
+ });
77
+ it('should return empty string when count is negative', () => {
78
+ expect(getTeamMemberTag(-1)).toBe('');
79
+ expect(getTeamMemberTag(-100)).toBe('');
80
+ });
81
+ it('should return [Team:1] when count is 1', () => {
82
+ expect(getTeamMemberTag(1)).toBe('\x1b[2m[Team:1]\x1b[0m');
83
+ });
84
+ it('should return [Team:4] when count is 4', () => {
85
+ expect(getTeamMemberTag(4)).toBe('\x1b[2m[Team:4]\x1b[0m');
86
+ });
87
+ it('should return [Team:10] when count is 10', () => {
88
+ expect(getTeamMemberTag(10)).toBe('\x1b[2m[Team:10]\x1b[0m');
89
+ });
90
+ });
91
+ describe('getStatusDisplay with team members', () => {
92
+ it('should append [Team:4] badge when teamMemberCount is 4', () => {
93
+ const result = getStatusDisplay('busy', 0, 4);
94
+ expect(result).toBe(`${STATUS_ICONS.BUSY} ${STATUS_LABELS.BUSY} \x1b[2m[Team:4]\x1b[0m`);
95
+ });
96
+ it('should append both [BG] and [Team:4] badges', () => {
97
+ const result = getStatusDisplay('busy', 1, 4);
98
+ expect(result).toBe(`${STATUS_ICONS.BUSY} ${STATUS_LABELS.BUSY} ${STATUS_TAGS.BACKGROUND_TASK} \x1b[2m[Team:4]\x1b[0m`);
99
+ });
100
+ it('should not append [Team] badge when teamMemberCount is 0', () => {
101
+ const result = getStatusDisplay('idle', 0, 0);
102
+ expect(result).toBe(`${STATUS_ICONS.IDLE} ${STATUS_LABELS.IDLE}`);
103
+ });
104
+ });
@@ -18,6 +18,7 @@ vi.mock('./stateDetector/index.js', () => ({
18
18
  createStateDetector: () => ({
19
19
  detectState: detectStateMock,
20
20
  detectBackgroundTask: () => false,
21
+ detectTeamMembers: () => 0,
21
22
  }),
22
23
  }));
23
24
  vi.mock('./config/configReader.js', () => ({
@@ -9,6 +9,7 @@ export interface SessionCounts {
9
9
  pending_auto_approval: number;
10
10
  total: number;
11
11
  backgroundTasks: number;
12
+ teamMembers: number;
12
13
  }
13
14
  export declare class SessionManager extends EventEmitter implements ISessionManager {
14
15
  sessions: Map<string, Session>;
@@ -18,6 +19,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
18
19
  private spawn;
19
20
  detectTerminalState(session: Session): SessionState;
20
21
  detectBackgroundTask(session: Session): number;
22
+ detectTeamMembers(session: Session): number;
21
23
  private getTerminalContent;
22
24
  private handleAutoApproval;
23
25
  private cancelAutoApprovalVerification;
@@ -13,8 +13,9 @@ import { ProcessError, ConfigError } from '../types/errors.js';
13
13
  import { autoApprovalVerifier } from './autoApprovalVerifier.js';
14
14
  import { logger } from '../utils/logger.js';
15
15
  import { Mutex, createInitialSessionStateData } from '../utils/mutex.js';
16
- import { getBackgroundTaskTag } from '../constants/statusIcons.js';
16
+ import { getBackgroundTaskTag, getTeamMemberTag, } from '../constants/statusIcons.js';
17
17
  import { getTerminalScreenContent } from '../utils/screenCapture.js';
18
+ import { injectTeammateMode } from '../utils/commandArgs.js';
18
19
  const { Terminal } = pkg;
19
20
  const execAsync = promisify(exec);
20
21
  const TERMINAL_CONTENT_MAX_LINES = 300;
@@ -48,6 +49,9 @@ export class SessionManager extends EventEmitter {
48
49
  detectBackgroundTask(session) {
49
50
  return session.stateDetector.detectBackgroundTask(session.terminal);
50
51
  }
52
+ detectTeamMembers(session) {
53
+ return session.stateDetector.detectTeamMembers(session.terminal);
54
+ }
51
55
  getTerminalContent(session) {
52
56
  // Use the new screen capture utility that correctly handles
53
57
  // both normal and alternate screen buffers
@@ -249,7 +253,7 @@ export class SessionManager extends EventEmitter {
249
253
  });
250
254
  }
251
255
  const command = preset.command;
252
- const args = preset.args || [];
256
+ const args = injectTeammateMode(preset.command, preset.args || [], preset.detectionStrategy);
253
257
  // Spawn the process - fallback will be handled by setupExitHandler
254
258
  const ptyProcess = await this.spawn(command, args, worktreePath);
255
259
  return this.createSessionInternal(worktreePath, ptyProcess, {
@@ -325,12 +329,19 @@ export class SessionManager extends EventEmitter {
325
329
  const devcontainerCmd = execParts[0] || 'devcontainer';
326
330
  const execArgs = execParts.slice(1);
327
331
  // Build fallback command for devcontainer
328
- const fallbackFullArgs = [...execArgs, '--', 'claude'];
332
+ const fallbackClaudeArgs = injectTeammateMode('claude', [], session.detectionStrategy);
333
+ const fallbackFullArgs = [
334
+ ...execArgs,
335
+ '--',
336
+ 'claude',
337
+ ...fallbackClaudeArgs,
338
+ ];
329
339
  fallbackProcess = await this.spawn(devcontainerCmd, fallbackFullArgs, session.worktreePath);
330
340
  }
331
341
  else {
332
342
  // Regular fallback without devcontainer
333
- fallbackProcess = await this.spawn('claude', [], session.worktreePath);
343
+ const fallbackArgs = injectTeammateMode('claude', [], session.detectionStrategy);
344
+ fallbackProcess = await this.spawn('claude', fallbackArgs, session.worktreePath);
334
345
  }
335
346
  // Replace the process
336
347
  session.process = fallbackProcess;
@@ -420,6 +431,14 @@ export class SessionManager extends EventEmitter {
420
431
  backgroundTaskCount,
421
432
  }));
422
433
  }
434
+ // Detect and update team member count
435
+ const teamMemberCount = this.detectTeamMembers(session);
436
+ if (currentStateData.teamMemberCount !== teamMemberCount) {
437
+ void session.stateMutex.update(data => ({
438
+ ...data,
439
+ teamMemberCount,
440
+ }));
441
+ }
423
442
  }, STATE_CHECK_INTERVAL_MS);
424
443
  // Setup exit handler
425
444
  this.setupExitHandler(session);
@@ -638,12 +657,8 @@ export class SessionManager extends EventEmitter {
638
657
  const devcontainerCmd = execParts[0] || 'devcontainer';
639
658
  const execArgs = execParts.slice(1);
640
659
  // Build the full command: devcontainer exec [args] -- [preset command] [preset args]
641
- const fullArgs = [
642
- ...execArgs,
643
- '--',
644
- preset.command,
645
- ...(preset.args || []),
646
- ];
660
+ const presetArgs = injectTeammateMode(preset.command, preset.args || [], preset.detectionStrategy);
661
+ const fullArgs = [...execArgs, '--', preset.command, ...presetArgs];
647
662
  // Spawn the process within devcontainer
648
663
  const ptyProcess = await this.spawn(devcontainerCmd, fullArgs, worktreePath);
649
664
  return this.createSessionInternal(worktreePath, ptyProcess, {
@@ -681,6 +696,7 @@ export class SessionManager extends EventEmitter {
681
696
  pending_auto_approval: 0,
682
697
  total: sessions.length,
683
698
  backgroundTasks: 0,
699
+ teamMembers: 0,
684
700
  };
685
701
  sessions.forEach(session => {
686
702
  const stateData = session.stateMutex.getSnapshot();
@@ -699,6 +715,7 @@ export class SessionManager extends EventEmitter {
699
715
  break;
700
716
  }
701
717
  counts.backgroundTasks += stateData.backgroundTaskCount;
718
+ counts.teamMembers += stateData.teamMemberCount;
702
719
  });
703
720
  return counts;
704
721
  }
@@ -721,6 +738,8 @@ export class SessionManager extends EventEmitter {
721
738
  }
722
739
  const bgTag = getBackgroundTaskTag(counts.backgroundTasks);
723
740
  const bgSuffix = bgTag ? ` ${bgTag}` : '';
724
- return ` (${parts.join(' / ')}${bgSuffix})`;
741
+ const teamTag = getTeamMemberTag(counts.teamMembers);
742
+ const teamSuffix = teamTag ? ` ${teamTag}` : '';
743
+ return ` (${parts.join(' / ')}${bgSuffix}${teamSuffix})`;
725
744
  }
726
745
  }
@@ -106,7 +106,7 @@ describe('SessionManager', () => {
106
106
  // Create session with preset
107
107
  await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
108
108
  // Verify spawn was called with preset config
109
- expect(spawn).toHaveBeenCalledWith('claude', ['--preset-arg'], {
109
+ expect(spawn).toHaveBeenCalledWith('claude', ['--preset-arg', '--teammate-mode', 'in-process'], {
110
110
  name: 'xterm-256color',
111
111
  cols: expect.any(Number),
112
112
  rows: expect.any(Number),
@@ -130,7 +130,7 @@ describe('SessionManager', () => {
130
130
  // Verify getPresetByIdEffect was called with correct ID
131
131
  expect(configReader.getPresetByIdEffect).toHaveBeenCalledWith('2');
132
132
  // Verify spawn was called with preset config
133
- expect(spawn).toHaveBeenCalledWith('claude', ['--resume', '--dev'], {
133
+ expect(spawn).toHaveBeenCalledWith('claude', ['--resume', '--dev', '--teammate-mode', 'in-process'], {
134
134
  name: 'xterm-256color',
135
135
  cols: expect.any(Number),
136
136
  rows: expect.any(Number),
@@ -156,7 +156,7 @@ describe('SessionManager', () => {
156
156
  await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree', 'invalid'));
157
157
  // Verify fallback to default preset
158
158
  expect(configReader.getDefaultPreset).toHaveBeenCalled();
159
- expect(spawn).toHaveBeenCalledWith('claude', [], expect.any(Object));
159
+ expect(spawn).toHaveBeenCalledWith('claude', ['--teammate-mode', 'in-process'], expect.any(Object));
160
160
  });
161
161
  it('should throw error when spawn fails with preset', async () => {
162
162
  // Setup mock preset with fallback
@@ -175,7 +175,7 @@ describe('SessionManager', () => {
175
175
  await expect(Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'))).rejects.toThrow('Command failed');
176
176
  // Verify only one spawn attempt was made
177
177
  expect(spawn).toHaveBeenCalledTimes(1);
178
- expect(spawn).toHaveBeenCalledWith('claude', ['--bad-flag'], expect.any(Object));
178
+ expect(spawn).toHaveBeenCalledWith('claude', ['--bad-flag', '--teammate-mode', 'in-process'], expect.any(Object));
179
179
  });
180
180
  it('should return existing session if already created', async () => {
181
181
  // Setup mock preset
@@ -229,14 +229,14 @@ describe('SessionManager', () => {
229
229
  const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
230
230
  // Verify initial spawn
231
231
  expect(spawn).toHaveBeenCalledTimes(1);
232
- expect(spawn).toHaveBeenCalledWith('claude', ['--invalid-flag'], expect.objectContaining({ cwd: '/test/worktree' }));
232
+ expect(spawn).toHaveBeenCalledWith('claude', ['--invalid-flag', '--teammate-mode', 'in-process'], expect.objectContaining({ cwd: '/test/worktree' }));
233
233
  // Simulate exit with code 1 on first attempt
234
234
  firstMockPty.emit('exit', { exitCode: 1 });
235
235
  // Wait for fallback to occur
236
236
  await new Promise(resolve => setTimeout(resolve, 50));
237
237
  // Verify fallback spawn was called (with no args since commandConfig was removed)
238
238
  expect(spawn).toHaveBeenCalledTimes(2);
239
- expect(spawn).toHaveBeenNthCalledWith(2, 'claude', [], expect.objectContaining({ cwd: '/test/worktree' }));
239
+ expect(spawn).toHaveBeenNthCalledWith(2, 'claude', ['--teammate-mode', 'in-process'], expect.objectContaining({ cwd: '/test/worktree' }));
240
240
  // Verify session process was replaced
241
241
  expect(session.process).toBe(secondMockPty);
242
242
  expect(session.isPrimaryCommand).toBe(false);
@@ -258,7 +258,7 @@ describe('SessionManager', () => {
258
258
  await new Promise(resolve => setTimeout(resolve, 600));
259
259
  // Verify only one spawn attempt
260
260
  expect(spawn).toHaveBeenCalledTimes(1);
261
- expect(spawn).toHaveBeenCalledWith('claude', ['--resume'], expect.objectContaining({ cwd: '/test/worktree' }));
261
+ expect(spawn).toHaveBeenCalledWith('claude', ['--resume', '--teammate-mode', 'in-process'], expect.objectContaining({ cwd: '/test/worktree' }));
262
262
  });
263
263
  it('should use empty args as fallback when no fallback args specified', async () => {
264
264
  // Setup mock preset without fallback args
@@ -280,15 +280,14 @@ describe('SessionManager', () => {
280
280
  const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
281
281
  // Verify initial spawn
282
282
  expect(spawn).toHaveBeenCalledTimes(1);
283
- expect(spawn).toHaveBeenCalledWith('claude', ['--invalid-flag'], expect.objectContaining({ cwd: '/test/worktree' }));
283
+ expect(spawn).toHaveBeenCalledWith('claude', ['--invalid-flag', '--teammate-mode', 'in-process'], expect.objectContaining({ cwd: '/test/worktree' }));
284
284
  // Simulate exit with code 1 on first attempt
285
285
  firstMockPty.emit('exit', { exitCode: 1 });
286
286
  // Wait for fallback to occur
287
287
  await new Promise(resolve => setTimeout(resolve, 50));
288
- // Verify fallback spawn was called with empty args
288
+ // Verify fallback spawn was called with teammate-mode args
289
289
  expect(spawn).toHaveBeenCalledTimes(2);
290
- expect(spawn).toHaveBeenNthCalledWith(2, 'claude', [], // Empty args
291
- expect.objectContaining({ cwd: '/test/worktree' }));
290
+ expect(spawn).toHaveBeenNthCalledWith(2, 'claude', ['--teammate-mode', 'in-process'], expect.objectContaining({ cwd: '/test/worktree' }));
292
291
  // Verify session process was replaced
293
292
  expect(session.process).toBe(secondMockPty);
294
293
  expect(session.isPrimaryCommand).toBe(false);
@@ -406,7 +405,16 @@ describe('SessionManager', () => {
406
405
  await Effect.runPromise(sessionManager.createSessionWithDevcontainerEffect('/test/worktree', devcontainerConfig));
407
406
  // Verify spawn was called correctly which proves devcontainer up succeeded
408
407
  // Verify spawn was called with devcontainer exec
409
- expect(spawn).toHaveBeenCalledWith('devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude', '--resume'], expect.objectContaining({ cwd: '/test/worktree' }));
408
+ expect(spawn).toHaveBeenCalledWith('devcontainer', [
409
+ 'exec',
410
+ '--workspace-folder',
411
+ '.',
412
+ '--',
413
+ 'claude',
414
+ '--resume',
415
+ '--teammate-mode',
416
+ 'in-process',
417
+ ], expect.objectContaining({ cwd: '/test/worktree' }));
410
418
  });
411
419
  it('should use specific preset when ID provided', async () => {
412
420
  // Setup mock preset
@@ -426,7 +434,15 @@ describe('SessionManager', () => {
426
434
  await Effect.runPromise(sessionManager.createSessionWithDevcontainerEffect('/test/worktree', devcontainerConfig, '2'));
427
435
  // Verify correct preset was used
428
436
  expect(configReader.getPresetByIdEffect).toHaveBeenCalledWith('2');
429
- expect(spawn).toHaveBeenCalledWith('devcontainer', ['exec', '--', 'claude', '--resume', '--dev'], expect.any(Object));
437
+ expect(spawn).toHaveBeenCalledWith('devcontainer', [
438
+ 'exec',
439
+ '--',
440
+ 'claude',
441
+ '--resume',
442
+ '--dev',
443
+ '--teammate-mode',
444
+ 'in-process',
445
+ ], expect.any(Object));
430
446
  });
431
447
  it('should throw error when devcontainer up fails', async () => {
432
448
  // Setup exec to fail
@@ -487,6 +503,8 @@ describe('SessionManager', () => {
487
503
  'claude',
488
504
  '--model',
489
505
  'opus',
506
+ '--teammate-mode',
507
+ 'in-process',
490
508
  ], expect.any(Object));
491
509
  });
492
510
  it('should spawn process with devcontainer exec command', async () => {
@@ -517,7 +535,15 @@ describe('SessionManager', () => {
517
535
  execCommand: 'devcontainer exec --workspace-folder .',
518
536
  }));
519
537
  // Should spawn with devcontainer exec command
520
- expect(spawn).toHaveBeenCalledWith('devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude'], expect.objectContaining({
538
+ expect(spawn).toHaveBeenCalledWith('devcontainer', [
539
+ 'exec',
540
+ '--workspace-folder',
541
+ '.',
542
+ '--',
543
+ 'claude',
544
+ '--teammate-mode',
545
+ 'in-process',
546
+ ], expect.objectContaining({
521
547
  cwd: '/test/worktree2',
522
548
  }));
523
549
  });
@@ -570,6 +596,8 @@ describe('SessionManager', () => {
570
596
  'vscode',
571
597
  '--',
572
598
  'claude',
599
+ '--teammate-mode',
600
+ 'in-process',
573
601
  ], expect.any(Object));
574
602
  });
575
603
  it('should handle preset with args in devcontainer', async () => {
@@ -602,6 +630,8 @@ describe('SessionManager', () => {
602
630
  'claude',
603
631
  '-m',
604
632
  'claude-3-opus',
633
+ '--teammate-mode',
634
+ 'in-process',
605
635
  ], expect.any(Object));
606
636
  });
607
637
  it('should use empty args as fallback in devcontainer when no fallback args specified', async () => {
@@ -637,15 +667,31 @@ describe('SessionManager', () => {
637
667
  }));
638
668
  // Verify initial spawn
639
669
  expect(spawn).toHaveBeenCalledTimes(1);
640
- expect(spawn).toHaveBeenCalledWith('devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude', '--invalid-flag'], expect.objectContaining({ cwd: '/test/worktree' }));
670
+ expect(spawn).toHaveBeenCalledWith('devcontainer', [
671
+ 'exec',
672
+ '--workspace-folder',
673
+ '.',
674
+ '--',
675
+ 'claude',
676
+ '--invalid-flag',
677
+ '--teammate-mode',
678
+ 'in-process',
679
+ ], expect.objectContaining({ cwd: '/test/worktree' }));
641
680
  // Simulate exit with code 1 on first attempt
642
681
  firstMockPty.emit('exit', { exitCode: 1 });
643
682
  // Wait for fallback to occur
644
683
  await new Promise(resolve => setTimeout(resolve, 50));
645
- // Verify fallback spawn was called with empty args
684
+ // Verify fallback spawn was called with teammate-mode args
646
685
  expect(spawn).toHaveBeenCalledTimes(2);
647
- expect(spawn).toHaveBeenNthCalledWith(2, 'devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude'], // No args after claude
648
- expect.objectContaining({ cwd: '/test/worktree' }));
686
+ expect(spawn).toHaveBeenNthCalledWith(2, 'devcontainer', [
687
+ 'exec',
688
+ '--workspace-folder',
689
+ '.',
690
+ '--',
691
+ 'claude',
692
+ '--teammate-mode',
693
+ 'in-process',
694
+ ], expect.objectContaining({ cwd: '/test/worktree' }));
649
695
  // Verify session process was replaced
650
696
  expect(session.process).toBe(secondMockPty);
651
697
  expect(session.isPrimaryCommand).toBe(false);
@@ -682,14 +728,31 @@ describe('SessionManager', () => {
682
728
  }));
683
729
  // Verify initial spawn
684
730
  expect(spawn).toHaveBeenCalledTimes(1);
685
- expect(spawn).toHaveBeenCalledWith('devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude', '--bad-flag'], expect.objectContaining({ cwd: '/test/worktree' }));
731
+ expect(spawn).toHaveBeenCalledWith('devcontainer', [
732
+ 'exec',
733
+ '--workspace-folder',
734
+ '.',
735
+ '--',
736
+ 'claude',
737
+ '--bad-flag',
738
+ '--teammate-mode',
739
+ 'in-process',
740
+ ], expect.objectContaining({ cwd: '/test/worktree' }));
686
741
  // Simulate exit with code 1 on first attempt
687
742
  firstMockPty.emit('exit', { exitCode: 1 });
688
743
  // Wait for fallback to occur
689
744
  await new Promise(resolve => setTimeout(resolve, 50));
690
- // Verify fallback spawn was called (with no args since commandConfig was removed)
745
+ // Verify fallback spawn was called with teammate-mode args
691
746
  expect(spawn).toHaveBeenCalledTimes(2);
692
- expect(spawn).toHaveBeenNthCalledWith(2, 'devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude'], expect.objectContaining({ cwd: '/test/worktree' }));
747
+ expect(spawn).toHaveBeenNthCalledWith(2, 'devcontainer', [
748
+ 'exec',
749
+ '--workspace-folder',
750
+ '.',
751
+ '--',
752
+ 'claude',
753
+ '--teammate-mode',
754
+ 'in-process',
755
+ ], expect.objectContaining({ cwd: '/test/worktree' }));
693
756
  // Verify session process was replaced
694
757
  expect(session.process).toBe(secondMockPty);
695
758
  expect(session.isPrimaryCommand).toBe(false);
@@ -781,10 +844,10 @@ describe('SessionManager', () => {
781
844
  describe('static methods', () => {
782
845
  describe('getSessionCounts', () => {
783
846
  // Helper to create mock session with stateMutex
784
- const createMockSession = (id, state, backgroundTaskCount = 0) => ({
847
+ const createMockSession = (id, state, backgroundTaskCount = 0, teamMemberCount = 0) => ({
785
848
  id,
786
849
  stateMutex: {
787
- getSnapshot: () => ({ state, backgroundTaskCount }),
850
+ getSnapshot: () => ({ state, backgroundTaskCount, teamMemberCount }),
788
851
  },
789
852
  });
790
853
  it('should count sessions by state', () => {
@@ -830,6 +893,15 @@ describe('SessionManager', () => {
830
893
  const counts = SessionManager.getSessionCounts(sessions);
831
894
  expect(counts.backgroundTasks).toBe(6);
832
895
  });
896
+ it('should sum team member counts across sessions', () => {
897
+ const sessions = [
898
+ createMockSession('1', 'idle', 0, 0),
899
+ createMockSession('2', 'busy', 0, 4),
900
+ createMockSession('3', 'busy', 0, 2),
901
+ ];
902
+ const counts = SessionManager.getSessionCounts(sessions);
903
+ expect(counts.teamMembers).toBe(6);
904
+ });
833
905
  });
834
906
  describe('formatSessionCounts', () => {
835
907
  it('should format counts with all states', () => {
@@ -840,6 +912,7 @@ describe('SessionManager', () => {
840
912
  pending_auto_approval: 0,
841
913
  total: 4,
842
914
  backgroundTasks: 0,
915
+ teamMembers: 0,
843
916
  };
844
917
  const formatted = SessionManager.formatSessionCounts(counts);
845
918
  expect(formatted).toBe(' (1 Idle / 2 Busy / 1 Waiting)');
@@ -852,6 +925,7 @@ describe('SessionManager', () => {
852
925
  pending_auto_approval: 0,
853
926
  total: 3,
854
927
  backgroundTasks: 0,
928
+ teamMembers: 0,
855
929
  };
856
930
  const formatted = SessionManager.formatSessionCounts(counts);
857
931
  expect(formatted).toBe(' (2 Idle / 1 Waiting)');
@@ -864,6 +938,7 @@ describe('SessionManager', () => {
864
938
  pending_auto_approval: 0,
865
939
  total: 3,
866
940
  backgroundTasks: 0,
941
+ teamMembers: 0,
867
942
  };
868
943
  const formatted = SessionManager.formatSessionCounts(counts);
869
944
  expect(formatted).toBe(' (3 Busy)');
@@ -876,6 +951,7 @@ describe('SessionManager', () => {
876
951
  pending_auto_approval: 0,
877
952
  total: 0,
878
953
  backgroundTasks: 0,
954
+ teamMembers: 0,
879
955
  };
880
956
  const formatted = SessionManager.formatSessionCounts(counts);
881
957
  expect(formatted).toBe('');
@@ -888,6 +964,7 @@ describe('SessionManager', () => {
888
964
  pending_auto_approval: 0,
889
965
  total: 2,
890
966
  backgroundTasks: 1,
967
+ teamMembers: 0,
891
968
  };
892
969
  const formatted = SessionManager.formatSessionCounts(counts);
893
970
  expect(formatted).toContain('[BG]');
@@ -901,6 +978,7 @@ describe('SessionManager', () => {
901
978
  pending_auto_approval: 0,
902
979
  total: 2,
903
980
  backgroundTasks: 5,
981
+ teamMembers: 0,
904
982
  };
905
983
  const formatted = SessionManager.formatSessionCounts(counts);
906
984
  expect(formatted).toContain('[BG:5]');
@@ -914,11 +992,41 @@ describe('SessionManager', () => {
914
992
  pending_auto_approval: 0,
915
993
  total: 2,
916
994
  backgroundTasks: 0,
995
+ teamMembers: 0,
917
996
  };
918
997
  const formatted = SessionManager.formatSessionCounts(counts);
919
998
  expect(formatted).not.toContain('[BG');
920
999
  expect(formatted).toBe(' (1 Idle / 1 Busy)');
921
1000
  });
1001
+ it('should append [Team:N] tag when teamMembers > 0', () => {
1002
+ const counts = {
1003
+ idle: 1,
1004
+ busy: 1,
1005
+ waiting_input: 0,
1006
+ pending_auto_approval: 0,
1007
+ total: 2,
1008
+ backgroundTasks: 0,
1009
+ teamMembers: 4,
1010
+ };
1011
+ const formatted = SessionManager.formatSessionCounts(counts);
1012
+ expect(formatted).toContain('[Team:4]');
1013
+ expect(formatted).toBe(' (1 Idle / 1 Busy \x1b[2m[Team:4]\x1b[0m)');
1014
+ });
1015
+ it('should append both [BG] and [Team:N] tags', () => {
1016
+ const counts = {
1017
+ idle: 1,
1018
+ busy: 1,
1019
+ waiting_input: 0,
1020
+ pending_auto_approval: 0,
1021
+ total: 2,
1022
+ backgroundTasks: 1,
1023
+ teamMembers: 4,
1024
+ };
1025
+ const formatted = SessionManager.formatSessionCounts(counts);
1026
+ expect(formatted).toContain('[BG]');
1027
+ expect(formatted).toContain('[Team:4]');
1028
+ expect(formatted).toBe(' (1 Idle / 1 Busy \x1b[2m[BG]\x1b[0m \x1b[2m[Team:4]\x1b[0m)');
1029
+ });
922
1030
  });
923
1031
  });
924
1032
  });
@@ -5,4 +5,5 @@ export declare abstract class BaseStateDetector implements StateDetector {
5
5
  protected getTerminalLines(terminal: Terminal, maxLines: number): string[];
6
6
  protected getTerminalContent(terminal: Terminal, maxLines: number): string;
7
7
  abstract detectBackgroundTask(terminal: Terminal): number;
8
+ abstract detectTeamMembers(terminal: Terminal): number;
8
9
  }
@@ -3,4 +3,5 @@ import { BaseStateDetector } from './base.js';
3
3
  export declare class ClaudeStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, currentState: SessionState): SessionState;
5
5
  detectBackgroundTask(terminal: Terminal): number;
6
+ detectTeamMembers(terminal: Terminal): number;
6
7
  }
@@ -45,4 +45,18 @@ export class ClaudeStateDetector extends BaseStateDetector {
45
45
  // No background task detected
46
46
  return 0;
47
47
  }
48
+ detectTeamMembers(terminal) {
49
+ const lines = this.getTerminalLines(terminal, 3);
50
+ // Look for the team member line containing "shift+↑ to expand"
51
+ const teamLine = lines.find(line => {
52
+ const lower = line.toLowerCase();
53
+ return (lower.includes('shift+↑ to expand') ||
54
+ lower.includes('shift+up to expand'));
55
+ });
56
+ if (!teamLine)
57
+ return 0;
58
+ // Extract @name patterns
59
+ const members = teamLine.match(/@[\w-]+/g);
60
+ return members ? members.length : 0;
61
+ }
48
62
  }
@@ -466,4 +466,68 @@ describe('ClaudeStateDetector', () => {
466
466
  expect(count).toBe(3);
467
467
  });
468
468
  });
469
+ describe('detectTeamMembers', () => {
470
+ it('should return 2 when two @name members are present with shift+↑ to expand', () => {
471
+ terminal = createMockTerminal([
472
+ 'Some output',
473
+ '@main @architect · shift+↑ to expand',
474
+ ]);
475
+ const count = detector.detectTeamMembers(terminal);
476
+ expect(count).toBe(2);
477
+ });
478
+ it('should return 4 when four @name members are present', () => {
479
+ terminal = createMockTerminal([
480
+ 'Some output',
481
+ '@main @architect @devils-advocate @ux-specialist · shift+↑ to expand',
482
+ ]);
483
+ const count = detector.detectTeamMembers(terminal);
484
+ expect(count).toBe(4);
485
+ });
486
+ it('should return 0 when no team line is present', () => {
487
+ terminal = createMockTerminal([
488
+ 'Command completed successfully',
489
+ 'Ready for next command',
490
+ '> ',
491
+ ]);
492
+ const count = detector.detectTeamMembers(terminal);
493
+ expect(count).toBe(0);
494
+ });
495
+ it('should return 0 when shift+↑ to expand is not present', () => {
496
+ terminal = createMockTerminal([
497
+ 'Some output with @mention',
498
+ 'Normal text',
499
+ ]);
500
+ const count = detector.detectTeamMembers(terminal);
501
+ expect(count).toBe(0);
502
+ });
503
+ it('should not match other shift+ shortcuts', () => {
504
+ terminal = createMockTerminal([
505
+ '@main @architect · shift+tab to auto-approve',
506
+ ]);
507
+ const count = detector.detectTeamMembers(terminal);
508
+ expect(count).toBe(0);
509
+ });
510
+ it('should return 0 for empty terminal', () => {
511
+ terminal = createMockTerminal([]);
512
+ const count = detector.detectTeamMembers(terminal);
513
+ expect(count).toBe(0);
514
+ });
515
+ it('should handle shift+up to expand variant', () => {
516
+ terminal = createMockTerminal(['@main @architect · shift+up to expand']);
517
+ const count = detector.detectTeamMembers(terminal);
518
+ expect(count).toBe(2);
519
+ });
520
+ it('should handle case-insensitive shift+↑ to expand', () => {
521
+ terminal = createMockTerminal(['@main @architect · SHIFT+↑ TO EXPAND']);
522
+ const count = detector.detectTeamMembers(terminal);
523
+ expect(count).toBe(2);
524
+ });
525
+ it('should handle @name patterns with hyphens', () => {
526
+ terminal = createMockTerminal([
527
+ '@team-lead @code-reviewer · shift+↑ to expand',
528
+ ]);
529
+ const count = detector.detectTeamMembers(terminal);
530
+ expect(count).toBe(2);
531
+ });
532
+ });
469
533
  });
@@ -3,4 +3,5 @@ import { BaseStateDetector } from './base.js';
3
3
  export declare class ClineStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
5
  detectBackgroundTask(_terminal: Terminal): number;
6
+ detectTeamMembers(_terminal: Terminal): number;
6
7
  }
@@ -24,4 +24,7 @@ export class ClineStateDetector extends BaseStateDetector {
24
24
  detectBackgroundTask(_terminal) {
25
25
  return 0;
26
26
  }
27
+ detectTeamMembers(_terminal) {
28
+ return 0;
29
+ }
27
30
  }
@@ -3,4 +3,5 @@ import { BaseStateDetector } from './base.js';
3
3
  export declare class CodexStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
5
  detectBackgroundTask(_terminal: Terminal): number;
6
+ detectTeamMembers(_terminal: Terminal): number;
6
7
  }
@@ -27,4 +27,7 @@ export class CodexStateDetector extends BaseStateDetector {
27
27
  detectBackgroundTask(_terminal) {
28
28
  return 0;
29
29
  }
30
+ detectTeamMembers(_terminal) {
31
+ return 0;
32
+ }
30
33
  }
@@ -3,4 +3,5 @@ import { BaseStateDetector } from './base.js';
3
3
  export declare class CursorStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
5
  detectBackgroundTask(_terminal: Terminal): number;
6
+ detectTeamMembers(_terminal: Terminal): number;
6
7
  }
@@ -19,4 +19,7 @@ export class CursorStateDetector extends BaseStateDetector {
19
19
  detectBackgroundTask(_terminal) {
20
20
  return 0;
21
21
  }
22
+ detectTeamMembers(_terminal) {
23
+ return 0;
24
+ }
22
25
  }
@@ -3,4 +3,5 @@ import { BaseStateDetector } from './base.js';
3
3
  export declare class GeminiStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
5
  detectBackgroundTask(_terminal: Terminal): number;
6
+ detectTeamMembers(_terminal: Terminal): number;
6
7
  }
@@ -28,4 +28,7 @@ export class GeminiStateDetector extends BaseStateDetector {
28
28
  detectBackgroundTask(_terminal) {
29
29
  return 0;
30
30
  }
31
+ detectTeamMembers(_terminal) {
32
+ return 0;
33
+ }
31
34
  }
@@ -3,4 +3,5 @@ import { BaseStateDetector } from './base.js';
3
3
  export declare class GitHubCopilotStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
5
  detectBackgroundTask(_terminal: Terminal): number;
6
+ detectTeamMembers(_terminal: Terminal): number;
6
7
  }
@@ -21,4 +21,7 @@ export class GitHubCopilotStateDetector extends BaseStateDetector {
21
21
  detectBackgroundTask(_terminal) {
22
22
  return 0;
23
23
  }
24
+ detectTeamMembers(_terminal) {
25
+ return 0;
26
+ }
24
27
  }
@@ -9,4 +9,5 @@ import { BaseStateDetector } from './base.js';
9
9
  export declare class KimiStateDetector extends BaseStateDetector {
10
10
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
11
11
  detectBackgroundTask(_terminal: Terminal): number;
12
+ detectTeamMembers(_terminal: Terminal): number;
12
13
  }
@@ -41,4 +41,7 @@ export class KimiStateDetector extends BaseStateDetector {
41
41
  // Kimi CLI does not currently support background tasks
42
42
  return 0;
43
43
  }
44
+ detectTeamMembers(_terminal) {
45
+ return 0;
46
+ }
44
47
  }
@@ -3,4 +3,5 @@ import { BaseStateDetector } from './base.js';
3
3
  export declare class OpenCodeStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
5
  detectBackgroundTask(_terminal: Terminal): number;
6
+ detectTeamMembers(_terminal: Terminal): number;
6
7
  }
@@ -17,4 +17,7 @@ export class OpenCodeStateDetector extends BaseStateDetector {
17
17
  detectBackgroundTask(_terminal) {
18
18
  return 0;
19
19
  }
20
+ detectTeamMembers(_terminal) {
21
+ return 0;
22
+ }
20
23
  }
@@ -2,4 +2,5 @@ import { SessionState, Terminal } from '../../types/index.js';
2
2
  export interface StateDetector {
3
3
  detectState(terminal: Terminal, currentState: SessionState): SessionState;
4
4
  detectBackgroundTask(terminal: Terminal): number;
5
+ detectTeamMembers(terminal: Terminal): number;
5
6
  }
@@ -0,0 +1,9 @@
1
+ import type { StateDetectionStrategy } from '../types/index.js';
2
+ /**
3
+ * Inject `--teammate-mode in-process` into args when running the `claude` command
4
+ * with the `claude` detection strategy. This prevents tmux conflicts when
5
+ * Claude Code's agent teams feature is used inside ccmanager's PTY-based sessions.
6
+ *
7
+ * Returns the original array unchanged if injection is not needed.
8
+ */
9
+ export declare function injectTeammateMode(command: string, args: string[], detectionStrategy: StateDetectionStrategy | undefined): string[];
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Inject `--teammate-mode in-process` into args when running the `claude` command
3
+ * with the `claude` detection strategy. This prevents tmux conflicts when
4
+ * Claude Code's agent teams feature is used inside ccmanager's PTY-based sessions.
5
+ *
6
+ * Returns the original array unchanged if injection is not needed.
7
+ */
8
+ export function injectTeammateMode(command, args, detectionStrategy) {
9
+ if (command === 'claude' &&
10
+ (detectionStrategy ?? 'claude') === 'claude' &&
11
+ !args.includes('--teammate-mode')) {
12
+ return [...args, '--teammate-mode', 'in-process'];
13
+ }
14
+ return args;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { injectTeammateMode } from './commandArgs.js';
3
+ describe('injectTeammateMode', () => {
4
+ it('should inject --teammate-mode in-process for claude command with claude strategy', () => {
5
+ const result = injectTeammateMode('claude', ['--resume'], 'claude');
6
+ expect(result).toEqual(['--resume', '--teammate-mode', 'in-process']);
7
+ });
8
+ it('should inject when detectionStrategy is undefined (defaults to claude)', () => {
9
+ const result = injectTeammateMode('claude', ['--resume'], undefined);
10
+ expect(result).toEqual(['--resume', '--teammate-mode', 'in-process']);
11
+ });
12
+ it('should append to existing args without mutating the original array', () => {
13
+ const original = ['--flag1', '--flag2'];
14
+ const result = injectTeammateMode('claude', original, 'claude');
15
+ expect(result).toEqual([
16
+ '--flag1',
17
+ '--flag2',
18
+ '--teammate-mode',
19
+ 'in-process',
20
+ ]);
21
+ expect(original).toEqual(['--flag1', '--flag2']);
22
+ expect(result).not.toBe(original);
23
+ });
24
+ it('should inject into empty args array', () => {
25
+ const result = injectTeammateMode('claude', [], undefined);
26
+ expect(result).toEqual(['--teammate-mode', 'in-process']);
27
+ });
28
+ it('should not inject when --teammate-mode is already present', () => {
29
+ const args = ['--teammate-mode', 'tmux'];
30
+ const result = injectTeammateMode('claude', args, 'claude');
31
+ expect(result).toEqual(['--teammate-mode', 'tmux']);
32
+ expect(result).toBe(args);
33
+ });
34
+ it('should not inject for non-claude command', () => {
35
+ const args = ['--resume'];
36
+ const result = injectTeammateMode('gemini', args, 'claude');
37
+ expect(result).toEqual(['--resume']);
38
+ expect(result).toBe(args);
39
+ });
40
+ it('should not inject for non-claude detection strategy', () => {
41
+ const args = ['--resume'];
42
+ const result = injectTeammateMode('claude', args, 'gemini');
43
+ expect(result).toEqual(['--resume']);
44
+ expect(result).toBe(args);
45
+ });
46
+ it('should not inject for custom command even with claude-like name', () => {
47
+ const args = ['--config', '/path'];
48
+ const result = injectTeammateMode('my-custom-claude', args, undefined);
49
+ expect(result).toEqual(['--config', '/path']);
50
+ expect(result).toBe(args);
51
+ });
52
+ });
@@ -48,6 +48,7 @@ export interface SessionStateData {
48
48
  autoApprovalReason: string | undefined;
49
49
  autoApprovalAbortController: AbortController | undefined;
50
50
  backgroundTaskCount: number;
51
+ teamMemberCount: number;
51
52
  }
52
53
  /**
53
54
  * Create initial session state data with default values.
@@ -86,5 +86,6 @@ export function createInitialSessionStateData() {
86
86
  autoApprovalReason: undefined,
87
87
  autoApprovalAbortController: undefined,
88
88
  backgroundTaskCount: 0,
89
+ teamMemberCount: 0,
89
90
  };
90
91
  }
@@ -82,7 +82,7 @@ export function prepareWorktreeItems(worktrees, sessions) {
82
82
  const session = sessions.find(s => s.worktreePath === wt.path);
83
83
  const stateData = session?.stateMutex.getSnapshot();
84
84
  const status = stateData
85
- ? ` [${getStatusDisplay(stateData.state, stateData.backgroundTaskCount)}]`
85
+ ? ` [${getStatusDisplay(stateData.state, stateData.backgroundTaskCount, stateData.teamMemberCount)}]`
86
86
  : '';
87
87
  const fullBranchName = wt.branch
88
88
  ? wt.branch.replace('refs/heads/', '')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "3.7.4",
3
+ "version": "3.8.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.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"
44
+ "@kodaikabasawa/ccmanager-darwin-arm64": "3.8.1",
45
+ "@kodaikabasawa/ccmanager-darwin-x64": "3.8.1",
46
+ "@kodaikabasawa/ccmanager-linux-arm64": "3.8.1",
47
+ "@kodaikabasawa/ccmanager-linux-x64": "3.8.1",
48
+ "@kodaikabasawa/ccmanager-win32-x64": "3.8.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.28.0",