Skip to content
Closed
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
22 changes: 22 additions & 0 deletions cli/src/claude/runClaude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,28 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
return;
}

if (specialCommand.type === 'model') {
logger.debug('[start] Detected /model command');
if (specialCommand.model == null) {
// /model with no arg: show current model
const modelName = currentModel ?? 'auto';
session.sendSessionEvent({ type: 'message', message: `Claude model: ${modelName}` });
} else {
// /model <name> or /model auto: set model
const newModel = specialCommand.model === 'auto' ? null : specialCommand.model;
currentModel = normalizeClaudeSessionModel(newModel);
currentSessionRef.current?.setModel(currentModel);
currentSessionRef.current?.pushKeepAlive();
const modelName = currentModel ?? 'auto';
session.sendSessionEvent({ type: 'message', message: `Claude model set to ${modelName}` });
logger.debug(`[start] /model command: model changed to ${modelName}`);
}
if (localId) {
session.emitMessagesConsumed([localId]);
}
return;
}

// Push with resolved permission mode, model, system prompts, and tools
const enhancedMode: EnhancedMode = {
permissionMode: messagePermissionMode ?? 'default',
Expand Down
1 change: 1 addition & 0 deletions cli/src/modules/common/slashCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const BUILTIN_COMMANDS: Record<string, SlashCommand[]> = {
{ name: 'compact', description: 'Compact conversation context', source: 'builtin' },
{ name: 'context', description: 'Show context information', source: 'builtin' },
{ name: 'cost', description: 'Show session cost', source: 'builtin' },
{ name: 'model', description: 'Show or set Claude model, e.g. /model claude-opus-4-7', source: 'builtin' },
{ name: 'plan', description: 'Toggle plan mode', source: 'builtin' },
],
codex: [
Expand Down
45 changes: 43 additions & 2 deletions cli/src/parsers/specialCommands.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { parseCompact, parseClear, parsePlan, parseSpecialCommand } from './specialCommands';
import { parseCompact, parseClear, parsePlan, parseModel, parseSpecialCommand } from './specialCommands';

describe('parseCompact', () => {
it('should parse /compact command with argument', () => {
Expand Down Expand Up @@ -92,6 +92,33 @@ describe('parsePlan', () => {
});
});

describe('parseModel', () => {
it('should parse /model without argument', () => {
const result = parseModel('/model');
expect(result).toEqual({ isModel: true, model: null });
});

it('should parse /model with a model name', () => {
const result = parseModel('/model claude-opus-4-7');
expect(result).toEqual({ isModel: true, model: 'claude-opus-4-7' });
});

it('should parse /model auto', () => {
const result = parseModel('/model auto');
expect(result).toEqual({ isModel: true, model: 'auto' });
});

it('should not parse /model with multiple arguments', () => {
const result = parseModel('/model foo bar');
expect(result.isModel).toBe(false);
});

it('should not parse partial matches', () => {
expect(parseModel('/modeler test').isModel).toBe(false);
expect(parseModel('please /model this').isModel).toBe(false);
});
});

describe('parseSpecialCommand', () => {
it('should detect compact command', () => {
const result = parseSpecialCommand('/compact optimize');
Expand All @@ -112,6 +139,18 @@ describe('parseSpecialCommand', () => {
expect(result.prompt).toBe('帮我规划五一行程');
});

it('should detect model command without argument', () => {
const result = parseSpecialCommand('/model');
expect(result.type).toBe('model');
expect(result.model).toBeNull();
});

it('should detect model command with a model name', () => {
const result = parseSpecialCommand('/model claude-sonnet-4-6');
expect(result.type).toBe('model');
expect(result.model).toBe('claude-sonnet-4-6');
});

it('should return null for regular messages', () => {
const result = parseSpecialCommand('hello world');
expect(result.type).toBeNull();
Expand All @@ -123,11 +162,13 @@ describe('parseSpecialCommand', () => {
expect(parseSpecialCommand(' /compact test ').type).toBe('compact');
expect(parseSpecialCommand(' /clear ').type).toBe('clear');
expect(parseSpecialCommand(' /plan test ').type).toBe('plan');

expect(parseSpecialCommand(' /model claude-opus-4-7 ').type).toBe('model');

// Test partial matches should not trigger
expect(parseSpecialCommand('some /compact text').type).toBeNull();
expect(parseSpecialCommand('/compactor').type).toBeNull();
expect(parseSpecialCommand('/clearing').type).toBeNull();
expect(parseSpecialCommand('/planner').type).toBeNull();
expect(parseSpecialCommand('/modeler').type).toBeNull();
});
});
36 changes: 33 additions & 3 deletions cli/src/parsers/specialCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ export interface PlanCommandResult {
prompt?: string;
}

export interface ModelCommandResult {
isModel: boolean;
model: string | null;
}

export interface SpecialCommandResult {
type: 'compact' | 'clear' | 'plan' | null;
type: 'compact' | 'clear' | 'plan' | 'model' | null;
originalMessage?: string;
mode?: 'plan' | 'default';
prompt?: string;
model?: string | null;
}

/**
Expand Down Expand Up @@ -109,6 +115,22 @@ export function parsePlan(message: string): PlanCommandResult {
};
}

/**
* Parse /model command
* - /model: show current model
* - /model <name>: set model to <name>
* - /model auto: reset to default model
*/
export function parseModel(message: string): ModelCommandResult {
const trimmed = message.trim();
const match = /^\/model(?:\s+(\S+))?$/i.exec(trimmed);
if (!match) {
return { isModel: false, model: null };
}
const arg = match[1]?.trim() ?? null;
return { isModel: true, model: arg };
}

/**
* Unified parser for special commands
* Returns the type of command and original message if applicable
Expand All @@ -121,7 +143,7 @@ export function parseSpecialCommand(message: string): SpecialCommandResult {
originalMessage: compactResult.originalMessage
};
}

const clearResult = parseClear(message);
if (clearResult.isClear) {
return {
Expand All @@ -138,7 +160,15 @@ export function parseSpecialCommand(message: string): SpecialCommandResult {
originalMessage: message.trim()
};
}


const modelResult = parseModel(message);
if (modelResult.isModel) {
return {
type: 'model',
model: modelResult.model
};
}

return {
type: null
};
Expand Down
Loading