Feature/lmstudio provider (#30)

* chore: just a plan update

* Add LM Studio as local AI provider (OpenAI-compatible, like Ollama)

* Convert WebP thumbnails to JPEG before image analysis for LM Studio compatibility

* Strengthen language enforcement in image analysis prompt for local models

* Use i18n localized prompts for image analysis instead of English instructions

* Add airplane mode (Flugmodus) with status bar toggle and offline model preferences

* Fix flightmode: persist model IDs, skip network when offline, airplane icon

* Auto-fallback to offline models in airplane mode for chat, title, and image analysis

* Auto-select first local model as offline fallback when no explicit offline model configured

* Block git fetch/pull/push and site upload in airplane mode

* fix: thumbnails optimized for AI

* fix: error handling in airplane mode

---------

Co-authored-by: hugo <hugoms@me.com>
This commit is contained in:
Georg Bauer
2026-03-02 13:35:42 +01:00
committed by GitHub
parent 4b4a9c1c8b
commit 5747925503
34 changed files with 2215 additions and 105 deletions

View File

@@ -333,6 +333,11 @@ vi.mock('fs/promises', () => ({
unlink: vi.fn(),
}));
let mockOfflineMode = false;
vi.mock('../../src/main/ipc/chatHandlers', () => ({
isOfflineModeActive: vi.fn(() => mockOfflineMode),
}));
// Helper to invoke a registered handler
async function invokeHandler(channel: string, ...args: any[]): Promise<any> {
const handler = registeredHandlers.get(channel);
@@ -383,6 +388,7 @@ describe('IPC Handlers', () => {
registeredHandlers.clear();
mockGeneratedFileHashStore.clear();
resetMockCounters();
mockOfflineMode = false;
// Create a real BlogGenerationEngine with mock engines for blog handler tests
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
@@ -513,6 +519,64 @@ describe('IPC Handlers', () => {
behind: 1,
});
});
it('should return zeroed state when offline mode is active', async () => {
mockOfflineMode = true;
const result = await invokeHandler('git:remoteState', '/repo');
expect(mockGitEngine.getRemoteState).not.toHaveBeenCalled();
expect(result).toEqual({ ahead: 0, behind: 0 });
});
});
describe('offline mode blocks network git operations', () => {
it('should block git:fetch when offline mode is active', async () => {
mockOfflineMode = true;
const result = await invokeHandler('git:fetch', '/repo');
expect(mockGitEngine.fetch).not.toHaveBeenCalled();
expect(result).toEqual({ success: false, code: 'offline' });
});
it('should block git:pull when offline mode is active', async () => {
mockOfflineMode = true;
const result = await invokeHandler('git:pull', '/repo');
expect(mockGitEngine.pull).not.toHaveBeenCalled();
expect(result).toEqual({ success: false, code: 'offline' });
});
it('should block git:push when offline mode is active', async () => {
mockOfflineMode = true;
const result = await invokeHandler('git:push', '/repo');
expect(mockGitEngine.push).not.toHaveBeenCalled();
expect(result).toEqual({ success: false, code: 'offline' });
});
it('should allow git:fetch when offline mode is inactive', async () => {
mockOfflineMode = false;
mockGitEngine.fetch.mockResolvedValue({ success: true });
const result = await invokeHandler('git:fetch', '/repo');
expect(mockGitEngine.fetch).toHaveBeenCalledWith('/repo');
expect(result).toEqual({ success: true });
});
it('should allow git:commitAll regardless of offline mode', async () => {
mockOfflineMode = true;
mockGitEngine.commitAll.mockResolvedValue({ success: true });
const result = await invokeHandler('git:commitAll', '/repo', 'test commit');
expect(mockGitEngine.commitAll).toHaveBeenCalledWith('/repo', 'test commit');
expect(result).toEqual({ success: true });
});
});
describe('git:diffContent', () => {
@@ -738,6 +802,21 @@ describe('IPC Handlers', () => {
});
});
// ============ Publish Handlers ============
describe('Publish Handlers', () => {
describe('publish:uploadSite offline guard', () => {
it('should throw when offline mode is active', async () => {
mockOfflineMode = true;
await expect(invokeHandler('publish:uploadSite', {
sshHost: 'example.com',
sshUser: 'deploy',
sshRemotePath: '/var/www',
})).rejects.toThrow('Airplane mode');
});
});
});
// ============ Project Handlers ============
describe('Project Handlers', () => {
describe('projects:create', () => {