ccmanager 3.7.4 → 3.8.0
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/README.md +10 -0
- package/dist/components/Menu.recent-projects.test.js +2 -0
- package/dist/components/Menu.test.js +2 -0
- package/dist/constants/statusIcons.d.ts +2 -1
- package/dist/constants/statusIcons.js +9 -2
- package/dist/constants/statusIcons.test.js +33 -1
- package/dist/services/sessionManager.autoApproval.test.js +1 -0
- package/dist/services/sessionManager.d.ts +2 -0
- package/dist/services/sessionManager.js +30 -11
- package/dist/services/sessionManager.test.js +131 -23
- package/dist/services/stateDetector/base.d.ts +1 -0
- package/dist/services/stateDetector/claude.d.ts +1 -0
- package/dist/services/stateDetector/claude.js +14 -0
- package/dist/services/stateDetector/claude.test.js +64 -0
- package/dist/services/stateDetector/cline.d.ts +1 -0
- package/dist/services/stateDetector/cline.js +3 -0
- package/dist/services/stateDetector/codex.d.ts +1 -0
- package/dist/services/stateDetector/codex.js +3 -0
- package/dist/services/stateDetector/cursor.d.ts +1 -0
- package/dist/services/stateDetector/cursor.js +3 -0
- package/dist/services/stateDetector/gemini.d.ts +1 -0
- package/dist/services/stateDetector/gemini.js +3 -0
- package/dist/services/stateDetector/github-copilot.d.ts +1 -0
- package/dist/services/stateDetector/github-copilot.js +3 -0
- package/dist/services/stateDetector/kimi.d.ts +1 -0
- package/dist/services/stateDetector/kimi.js +3 -0
- package/dist/services/stateDetector/opencode.d.ts +1 -0
- package/dist/services/stateDetector/opencode.js +3 -0
- package/dist/services/stateDetector/types.d.ts +1 -0
- package/dist/utils/commandArgs.d.ts +9 -0
- package/dist/utils/commandArgs.js +15 -0
- package/dist/utils/commandArgs.test.d.ts +1 -0
- package/dist/utils/commandArgs.test.js +52 -0
- package/dist/utils/mutex.d.ts +1 -0
- package/dist/utils/mutex.js +1 -0
- package/dist/utils/worktreeUtils.js +1 -1
- 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
|
|
|
@@ -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
|
-
|
|
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
|
+
});
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
642
|
-
|
|
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
|
-
|
|
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
|
|
288
|
+
// Verify fallback spawn was called with teammate-mode args
|
|
289
289
|
expect(spawn).toHaveBeenCalledTimes(2);
|
|
290
|
-
expect(spawn).toHaveBeenNthCalledWith(2, 'claude', [],
|
|
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', [
|
|
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', [
|
|
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', [
|
|
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', [
|
|
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
|
|
684
|
+
// Verify fallback spawn was called with teammate-mode args
|
|
646
685
|
expect(spawn).toHaveBeenCalledTimes(2);
|
|
647
|
-
expect(spawn).toHaveBeenNthCalledWith(2, 'devcontainer', [
|
|
648
|
-
|
|
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', [
|
|
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
|
|
745
|
+
// Verify fallback spawn was called with teammate-mode args
|
|
691
746
|
expect(spawn).toHaveBeenCalledTimes(2);
|
|
692
|
-
expect(spawn).toHaveBeenNthCalledWith(2, 'devcontainer', [
|
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
});
|
package/dist/utils/mutex.d.ts
CHANGED
|
@@ -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.
|
package/dist/utils/mutex.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "3.8.0",
|
|
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.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "3.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "3.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "3.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "3.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "3.8.0",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.8.0",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.8.0",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.8.0",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.8.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|