diff --git a/src/main/services/SettingsService.ts b/src/main/services/SettingsService.ts index 1c091f6..3b61286 100644 --- a/src/main/services/SettingsService.ts +++ b/src/main/services/SettingsService.ts @@ -17,7 +17,11 @@ const SettingsSchema = z.object({ // v0.3.0 Cut E — 양방향 git sync 설정. 모두 optional — 미구성 시 sync 비활성. sync_repo_url: z.string().nullable().optional(), sync_auto_enabled: z.boolean().optional(), - sync_interval_min: z.number().int().min(5).optional() + sync_interval_min: z.number().int().min(5).optional(), + // v0.3.1 Cut F + vision_model: z.string().nullable().optional(), + vision_capable_cache: z.array(z.string()).optional(), + vision_cache_at: z.string().optional() }).strict(); export type Settings = z.infer; @@ -127,6 +131,30 @@ export class SettingsService { await this.persist(next); } + /** v0.3.1 Cut F — 선택된 vision model. null = 미선택. */ + async getVisionModel(): Promise { + const s = await this.load(); + return s.vision_model ?? null; + } + + async setVisionModel(value: string | null): Promise { + const current = await this.load(); + const next: Settings = { ...current, vision_model: value }; + await this.persist(next); + } + + /** v0.3.1 Cut F — /api/tags 조회 결과 캐시. 기본 빈 배열 + null timestamp. */ + async getVisionCapableCache(): Promise<{ models: string[]; at: string | null }> { + const s = await this.load(); + return { models: s.vision_capable_cache ?? [], at: s.vision_cache_at ?? null }; + } + + async setVisionCapableCache(models: string[], now: Date): Promise { + const current = await this.load(); + const next: Settings = { ...current, vision_capable_cache: models, vision_cache_at: now.toISOString() }; + await this.persist(next); + } + private async persist(next: Settings): Promise { await mkdir(dirname(this.filePath), { recursive: true }); const tmpPath = this.filePath + '.tmp'; diff --git a/tests/unit/SettingsService.test.ts b/tests/unit/SettingsService.test.ts index 89fa152..c1e7343 100644 --- a/tests/unit/SettingsService.test.ts +++ b/tests/unit/SettingsService.test.ts @@ -90,4 +90,31 @@ describe('SettingsService', () => { await expect(svc.setSyncIntervalMin(10.5)).rejects.toThrow(); }); }); + + describe('v0.3.1 Cut F — vision settings', () => { + it('getVisionModel() defaults to null', async () => { + expect(await svc.getVisionModel()).toBeNull(); + }); + + it('setVisionModel() / getVisionModel() round-trip including null clear', async () => { + await svc.setVisionModel('llava:13b'); + expect(await svc.getVisionModel()).toBe('llava:13b'); + await svc.setVisionModel(null); + expect(await svc.getVisionModel()).toBeNull(); + }); + + it('getVisionCapableCache() defaults to empty models + null at', async () => { + const cache = await svc.getVisionCapableCache(); + expect(cache.models).toEqual([]); + expect(cache.at).toBeNull(); + }); + + it('setVisionCapableCache() persists models + ISO timestamp', async () => { + const now = new Date('2026-05-09T12:00:00.000Z'); + await svc.setVisionCapableCache(['llava:13b', 'llava:7b'], now); + const cache = await svc.getVisionCapableCache(); + expect(cache.models).toEqual(['llava:13b', 'llava:7b']); + expect(cache.at).toBe('2026-05-09T12:00:00.000Z'); + }); + }); });