feat: implement cursor model migration and enhance auto mode functionality

This commit introduces significant updates to the cursor model handling and auto mode features. The cursor model IDs have been standardized to a canonical format, ensuring backward compatibility while migrating legacy IDs. New endpoints for starting and stopping the auto mode loop have been added, allowing for better control over project-specific auto mode operations.

Key changes:
- Updated cursor model IDs to use the 'cursor-' prefix for consistency.
- Added new API endpoints: `/start` and `/stop` for managing auto mode.
- Enhanced the status endpoint to provide detailed project-specific auto mode information.
- Improved error handling and logging throughout the auto mode service.
- Migrated legacy model IDs to their canonical counterparts in various components.

This update aims to streamline the user experience and ensure a smooth transition for existing users while providing new functionalities.
This commit is contained in:
webdevcody
2026-01-18 18:42:52 -05:00
parent 3faebfa3fe
commit 4b0d1399b1
36 changed files with 1508 additions and 592 deletions

View File

@@ -37,7 +37,7 @@ describe('model-resolver.ts', () => {
const result = resolveModelString('opus');
expect(result).toBe('claude-opus-4-5-20251101');
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining('Resolved Claude model alias: "opus"')
expect.stringContaining('Migrated legacy ID: "opus" -> "claude-opus"')
);
});

View File

@@ -50,8 +50,8 @@ describe('cursor-config-manager.ts', () => {
manager = new CursorConfigManager(testProjectPath);
const config = manager.getConfig();
expect(config.defaultModel).toBe('auto');
expect(config.models).toContain('auto');
expect(config.defaultModel).toBe('cursor-auto');
expect(config.models).toContain('cursor-auto');
});
it('should use default config if file read fails', () => {
@@ -62,7 +62,7 @@ describe('cursor-config-manager.ts', () => {
manager = new CursorConfigManager(testProjectPath);
expect(manager.getDefaultModel()).toBe('auto');
expect(manager.getDefaultModel()).toBe('cursor-auto');
});
it('should use default config if JSON parse fails', () => {
@@ -71,7 +71,7 @@ describe('cursor-config-manager.ts', () => {
manager = new CursorConfigManager(testProjectPath);
expect(manager.getDefaultModel()).toBe('auto');
expect(manager.getDefaultModel()).toBe('cursor-auto');
});
});
@@ -93,7 +93,7 @@ describe('cursor-config-manager.ts', () => {
});
it('should return default model', () => {
expect(manager.getDefaultModel()).toBe('auto');
expect(manager.getDefaultModel()).toBe('cursor-auto');
});
it('should set and persist default model', () => {
@@ -103,13 +103,13 @@ describe('cursor-config-manager.ts', () => {
expect(fs.writeFileSync).toHaveBeenCalled();
});
it('should return auto if defaultModel is undefined', () => {
it('should return cursor-auto if defaultModel is undefined', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ models: ['auto'] }));
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ models: ['cursor-auto'] }));
manager = new CursorConfigManager(testProjectPath);
expect(manager.getDefaultModel()).toBe('auto');
expect(manager.getDefaultModel()).toBe('cursor-auto');
});
});
@@ -121,7 +121,7 @@ describe('cursor-config-manager.ts', () => {
it('should return enabled models', () => {
const models = manager.getEnabledModels();
expect(Array.isArray(models)).toBe(true);
expect(models).toContain('auto');
expect(models).toContain('cursor-auto');
});
it('should set enabled models', () => {
@@ -131,13 +131,13 @@ describe('cursor-config-manager.ts', () => {
expect(fs.writeFileSync).toHaveBeenCalled();
});
it('should return [auto] if models is undefined', () => {
it('should return [cursor-auto] if models is undefined', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ defaultModel: 'auto' }));
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ defaultModel: 'cursor-auto' }));
manager = new CursorConfigManager(testProjectPath);
expect(manager.getEnabledModels()).toEqual(['auto']);
expect(manager.getEnabledModels()).toEqual(['cursor-auto']);
});
});
@@ -146,8 +146,8 @@ describe('cursor-config-manager.ts', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(
JSON.stringify({
defaultModel: 'auto',
models: ['auto'],
defaultModel: 'cursor-auto',
models: ['cursor-auto'],
})
);
manager = new CursorConfigManager(testProjectPath);
@@ -161,14 +161,14 @@ describe('cursor-config-manager.ts', () => {
});
it('should not add duplicate models', () => {
manager.addModel('auto');
manager.addModel('cursor-auto');
// Should not save if model already exists
expect(fs.writeFileSync).not.toHaveBeenCalled();
});
it('should initialize models array if undefined', () => {
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ defaultModel: 'auto' }));
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ defaultModel: 'cursor-auto' }));
manager = new CursorConfigManager(testProjectPath);
manager.addModel('claude-3-5-sonnet');
@@ -293,7 +293,7 @@ describe('cursor-config-manager.ts', () => {
it('should reset to default values', () => {
manager.reset();
expect(manager.getDefaultModel()).toBe('auto');
expect(manager.getDefaultModel()).toBe('cursor-auto');
expect(manager.getMcpServers()).toEqual([]);
expect(manager.getRules()).toEqual([]);
expect(fs.writeFileSync).toHaveBeenCalled();

View File

@@ -647,9 +647,10 @@ describe('settings-service.ts', () => {
const settings = await settingsService.getGlobalSettings();
// Verify all phase models are now PhaseModelEntry objects
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'sonnet' });
expect(settings.phaseModels.fileDescriptionModel).toEqual({ model: 'haiku' });
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'opus' });
// Legacy aliases are migrated to canonical IDs
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'claude-sonnet' });
expect(settings.phaseModels.fileDescriptionModel).toEqual({ model: 'claude-haiku' });
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'claude-opus' });
expect(settings.version).toBe(SETTINGS_VERSION);
});
@@ -675,16 +676,17 @@ describe('settings-service.ts', () => {
const settings = await settingsService.getGlobalSettings();
// Verify PhaseModelEntry objects are preserved with thinkingLevel
// Legacy aliases are migrated to canonical IDs
expect(settings.phaseModels.enhancementModel).toEqual({
model: 'sonnet',
model: 'claude-sonnet',
thinkingLevel: 'high',
});
expect(settings.phaseModels.specGenerationModel).toEqual({
model: 'opus',
model: 'claude-opus',
thinkingLevel: 'ultrathink',
});
expect(settings.phaseModels.backlogPlanningModel).toEqual({
model: 'sonnet',
model: 'claude-sonnet',
thinkingLevel: 'medium',
});
});
@@ -710,15 +712,15 @@ describe('settings-service.ts', () => {
const settings = await settingsService.getGlobalSettings();
// Strings should be converted to objects
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'sonnet' });
expect(settings.phaseModels.imageDescriptionModel).toEqual({ model: 'haiku' });
// Objects should be preserved
// Strings should be converted to objects with canonical IDs
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'claude-sonnet' });
expect(settings.phaseModels.imageDescriptionModel).toEqual({ model: 'claude-haiku' });
// Objects should be preserved with migrated IDs
expect(settings.phaseModels.fileDescriptionModel).toEqual({
model: 'haiku',
model: 'claude-haiku',
thinkingLevel: 'low',
});
expect(settings.phaseModels.validationModel).toEqual({ model: 'opus' });
expect(settings.phaseModels.validationModel).toEqual({ model: 'claude-opus' });
});
it('should migrate legacy enhancementModel/validationModel fields', async () => {
@@ -735,11 +737,11 @@ describe('settings-service.ts', () => {
const settings = await settingsService.getGlobalSettings();
// Legacy fields should be migrated to phaseModels
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'haiku' });
expect(settings.phaseModels.validationModel).toEqual({ model: 'opus' });
// Other fields should use defaults
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'opus' });
// Legacy fields should be migrated to phaseModels with canonical IDs
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'claude-haiku' });
expect(settings.phaseModels.validationModel).toEqual({ model: 'claude-opus' });
// Other fields should use defaults (canonical IDs)
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'claude-opus' });
});
it('should use default phase models when none are configured', async () => {
@@ -753,10 +755,10 @@ describe('settings-service.ts', () => {
const settings = await settingsService.getGlobalSettings();
// Should use DEFAULT_PHASE_MODELS
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'sonnet' });
expect(settings.phaseModels.fileDescriptionModel).toEqual({ model: 'haiku' });
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'opus' });
// Should use DEFAULT_PHASE_MODELS (with canonical IDs)
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'claude-sonnet' });
expect(settings.phaseModels.fileDescriptionModel).toEqual({ model: 'claude-haiku' });
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'claude-opus' });
});
it('should deep merge phaseModels on update', async () => {
@@ -776,13 +778,13 @@ describe('settings-service.ts', () => {
const settings = await settingsService.getGlobalSettings();
// Both should be preserved
// Both should be preserved (models migrated to canonical format)
expect(settings.phaseModels.enhancementModel).toEqual({
model: 'sonnet',
model: 'claude-sonnet',
thinkingLevel: 'high',
});
expect(settings.phaseModels.specGenerationModel).toEqual({
model: 'opus',
model: 'claude-opus',
thinkingLevel: 'ultrathink',
});
});