Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ CCManager supports multiple AI coding assistants with tailored state detection f
| Cline CLI | `cline` | [github.com/cline/cline](https://github.com/cline/cline) |
| OpenCode | `opencode` | [opencode.ai/docs](https://opencode.ai/docs) |
| Kimi CLI | `kimi` | [kimi-cli.com](https://www.kimi-cli.com/en/) |
| MiniMax Code (MCode) | `mcode` | [npmjs.com/package/@minimax-ai/code](https://www.npmjs.com/package/@minimax-ai/code) |

Each assistant has its own state detection strategy to properly track:
- **Idle**: Ready for new input
Expand Down
4 changes: 4 additions & 0 deletions src/components/ConfigureCommand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const createStrategyItems = (): {
cline: {label: 'Cline', value: 'cline'},
opencode: {label: 'OpenCode', value: 'opencode'},
kimi: {label: 'Kimi', value: 'kimi'},
mcode: {label: 'MiniMax Code', value: 'mcode'},
};

return Object.values(strategies);
Expand All @@ -50,6 +51,7 @@ const DEFAULT_COMMANDS: Record<StateDetectionStrategy, string> = {
cline: 'cline',
opencode: 'opencode',
kimi: 'kimi',
mcode: 'mcode',
};

interface ConfigureCommandProps {
Expand Down Expand Up @@ -81,6 +83,8 @@ const formatDetectionStrategy = (strategy: string | undefined): string => {
return 'OpenCode';
case 'kimi':
return 'Kimi';
case 'mcode':
return 'MiniMax Code';
default:
return 'Claude';
}
Expand Down
3 changes: 3 additions & 0 deletions src/services/stateDetector/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {GitHubCopilotStateDetector} from './github-copilot.js';
import {ClineStateDetector} from './cline.js';
import {OpenCodeStateDetector} from './opencode.js';
import {KimiStateDetector} from './kimi.js';
import {MCodeStateDetector} from './mcode.js';

export function createStateDetector(
strategy: StateDetectionStrategy = 'claude',
Expand All @@ -29,6 +30,8 @@ export function createStateDetector(
return new OpenCodeStateDetector();
case 'kimi':
return new KimiStateDetector();
case 'mcode':
return new MCodeStateDetector();
default:
return new ClaudeStateDetector();
}
Expand Down
72 changes: 72 additions & 0 deletions src/services/stateDetector/mcode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {beforeEach, describe, expect, it} from 'vitest';
import type {Terminal} from '../../types/index.js';
import {createStateDetector} from './index.js';
import {MCodeStateDetector} from './mcode.js';
import {createMockTerminal} from './testUtils.js';

describe('MCodeStateDetector', () => {
let detector: MCodeStateDetector;
let terminal: Terminal;

beforeEach(() => {
detector = new MCodeStateDetector();
});

it('detects the published MCode ready footer as idle', () => {
terminal = createMockTerminal([
'╭─ v0.1.2 ● Ready ─╮',
'│ Start · @ file · / autocomplete │',
'╰─────────────────────────────────────────╯',
]);

expect(detector.detectState(terminal, 'busy')).toBe('idle');
});

it('is selected by the mcode strategy', () => {
expect(createStateDetector('mcode')).toBeInstanceOf(MCodeStateDetector);
});

it('detects an active runtime turn as busy', () => {
terminal = createMockTerminal([
'session: mvs_abc | turn: turn_123 running 3s | local-runtime',
]);

expect(detector.detectState(terminal, 'idle')).toBe('busy');
});

it('detects the compact running status as busy', () => {
terminal = createMockTerminal(['session: mvs_abc | turn: running']);

expect(detector.detectState(terminal, 'idle')).toBe('busy');
});

it.each([
'permission: pending',
'[permission] permission.ask',
'ask_user: pending',
'[ask_user] questionnaire.ask',
])('detects %s as waiting for input', marker => {
terminal = createMockTerminal([
'session: mvs_abc | turn: turn_123 running 3s',
marker,
]);

expect(detector.detectState(terminal, 'busy')).toBe('waiting_input');
});

it('does not mistake historical thinking text for an active turn', () => {
terminal = createMockTerminal([
'Assistant: Thinking through the previous request...',
'╭─ v0.1.2 ● Ready ─╮',
]);

expect(detector.detectState(terminal, 'busy')).toBe('idle');
});

it('reports no background tasks or team members', () => {
terminal = createMockTerminal([]);

expect(detector.detectBackgroundTask(terminal)).toBe(0);
expect(detector.detectTeamMembers(terminal)).toBe(0);
});
});
43 changes: 43 additions & 0 deletions src/services/stateDetector/mcode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {SessionState, Terminal} from '../../types/index.js';
import {BaseStateDetector} from './base.js';

/**
* State detector for MiniMax Code (MCode).
* Command: mcode
* Installation: npm install -g @minimax-ai/code
*/
export class MCodeStateDetector extends BaseStateDetector {
detectState(terminal: Terminal, _currentState: SessionState): SessionState {
const content = this.getTerminalContent(terminal, 30);
const lowerContent = content.toLowerCase();

// MCode exposes pending interactive decisions in its runtime status and
// renders the event marker in the transcript. These take precedence over
// an active turn because the user must act before that turn can continue.
if (
lowerContent.includes('permission: pending') ||
lowerContent.includes('[permission] permission.ask') ||
lowerContent.includes('ask_user: pending') ||
lowerContent.includes('[ask_user] questionnaire.ask')
) {
return 'waiting_input';
}

// Current releases include a turn id; the compact fallback is emitted
// while a turn is starting. Match the status field rather than ordinary
// assistant text so historical words such as "thinking" cannot look busy.
if (/\bturn:\s+(?:\S+\s+)?running\b/i.test(content)) {
return 'busy';
}

return 'idle';
}

detectBackgroundTask(_terminal: Terminal): number {
return 0;
}

detectTeamMembers(_terminal: Terminal): number {
return 0;
}
}
3 changes: 2 additions & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export type StateDetectionStrategy =
| 'github-copilot'
| 'cline'
| 'opencode'
| 'kimi';
| 'kimi'
| 'mcode';

export interface Worktree {
path: string;
Expand Down
30 changes: 30 additions & 0 deletions src/utils/presetPrompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ describe('presetPrompt', () => {
});
});

it('uses the final argument for MCode presets', () => {
expect(
preparePresetLaunch(
{command: 'mcode', args: [], detectionStrategy: 'mcode'},
'fix the failing tests',
),
).toEqual({
args: ['fix the failing tests'],
method: 'final-arg',
});
});

describe('describePromptInjection', () => {
it('describes final-arg for claude', () => {
expect(
Expand Down Expand Up @@ -174,6 +186,15 @@ describe('presetPrompt', () => {
}),
).toContain('-p');
});

it('describes final-arg for MCode', () => {
expect(
describePromptInjection({
command: 'mcode',
detectionStrategy: 'mcode',
}),
).toContain('final command argument');
});
});

describe('getPromptInjectionMethod', () => {
Expand Down Expand Up @@ -249,6 +270,15 @@ describe('presetPrompt', () => {
).toBe('flag');
});

it('returns final-arg for MCode', () => {
expect(
getPromptInjectionMethod({
command: 'mcode',
detectionStrategy: 'mcode',
}),
).toBe('final-arg');
});

it('falls back to claude strategy when detectionStrategy is not set', () => {
expect(getPromptInjectionMethod({command: 'claude'})).toBe('final-arg');
});
Expand Down
3 changes: 2 additions & 1 deletion src/utils/presetPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export const getPromptInjectionMethod = (
strategy === 'claude' ||
strategy === 'codex' ||
strategy === 'cursor' ||
strategy === 'cline'
strategy === 'cline' ||
strategy === 'mcode'
) {
return 'final-arg';
}
Expand Down
Loading