feat: Add Ollama Cloud integration with 20+ free AI models

- Added AI Model Manager to sidebar for quick model switching
- Integrated Ollama Cloud API with official models from ollama.com
- Added AISettingsModal with searchable model catalog
- Models include: GPT-OSS 120B, DeepSeek V3.2, Gemini 3 Pro, Qwen3 Coder, etc.
- Added 'Get Key' button linking to ollama.com/settings/keys
- Updated README with Ollama Cloud documentation and free API key instructions
- Fixed ChatPanel export issue
- Added Brain icon for reasoning models
This commit is contained in:
Gemini AI
2025-12-20 13:01:52 +04:00
Unverified
parent 33261fdc56
commit e52098f3a8
10 changed files with 699 additions and 412 deletions

View File

@@ -145,19 +145,57 @@ OpenQode/
---
## 🔐 Authentication
## 🔐 AI Models & Authentication
OpenQode uses **Qwen AI** for its language model capabilities. Authentication is handled automatically:
OpenQode supports **multiple AI providers** for maximum flexibility:
### 🟢 Qwen Cloud (Default - Free)
Alibaba's powerful AI models with free tier access:
1. **First Launch** - The app will prompt you to authenticate
2. **Browser Login** - A browser window opens for Qwen login
3. **Token Storage** - Credentials are stored locally and encrypted
4. **Auto-Refresh** - Tokens are refreshed automatically
Available Qwen models: `qwen-coder-plus`, `qwen-plus`, `qwen-turbo`
If you need to re-authenticate, use **Option 5** in the launcher menu.
---
### 🔵 Ollama Cloud (Free - 20+ Models)
Access **state-of-the-art open-weight models** for FREE via Ollama Cloud:
#### 🎁 Get Your Free API Key:
1. Go to **[ollama.com/settings/keys](https://ollama.com/settings/keys)**
2. Sign in or create a free account
3. Generate an API key
4. Paste it in Goose Ultra's **AI Model Manager** (sidebar → AI Models)
#### 🚀 Available Free Models:
| Model | Size | Best For |
|-------|------|----------|
| **GPT-OSS 120B** | 120B | OpenAI's open-weight reasoning model |
| **DeepSeek V3.2** | MoE | Superior reasoning & agent performance |
| **Gemini 3 Pro Preview** | Cloud | Google's SOTA reasoning model |
| **Qwen3 Coder 480B** | 480B | Agentic coding, long context |
| **Devstral 2 123B** | 123B | Multi-file editing, software agents |
| **Kimi K2** | MoE | State-of-the-art coding agent tasks |
| **Qwen3 VL 235B** | 235B | Vision + language understanding |
| **Gemini 3 Flash** | Cloud | Fast, frontier intelligence |
| **Ministral 3** | 3-14B | Edge deployment, fast responses |
...and many more! Open the **AI Model Manager** in Goose Ultra to see all available models.
#### 📖 Ollama Cloud Docs:
- API Documentation: [docs.ollama.com/cloud](https://docs.ollama.com/cloud)
- Model Library: [ollama.com/search?c=cloud](https://ollama.com/search?c=cloud)
---
## 🐛 Troubleshooting
### "Node.js not found"

View File

@@ -1,7 +1,8 @@
import { app, BrowserWindow, ipcMain, shell, protocol, net } from 'electron';
import path from 'path';
import { fileURLToPath } from 'url';
import { streamChat } from './qwen-api.js';
import { streamChat as qwenStreamChat } from './qwen-api.js';
import * as ollamaApi from './ollama-api.js';
import { generateImage, detectImageRequest, cleanupCache } from './image-api.js';
import { fsApi } from './fs-api.js';
import * as viAutomation from './vi-automation.js';
@@ -172,22 +173,53 @@ ipcMain.handle('export-project-zip', async (_, { projectId }) => {
});
// Chat Streaming IPC
ipcMain.on('chat-stream-start', (event, { messages, model }) => {
ipcMain.on('chat-stream-start', async (event, { messages, model }) => {
const window = BrowserWindow.fromWebContents(event.sender);
streamChat(
messages,
model,
(chunk) => {
if (!window.isDestroyed()) {
// console.log('[Main] Sending chunk size:', chunk.length); // Verbose log
window.webContents.send('chat-chunk', chunk);
// Choose provider based on model prefix or name
// Default to qwen unless model starts with 'ollama:' or matches known ollama models
const isOllama = model?.startsWith('ollama:') || model === 'gpt-oss:120b';
const cleanModel = isOllama ? model.replace('ollama:', '') : model;
const onChunk = (chunk) => {
if (!window.isDestroyed()) window.webContents.send('chat-chunk', chunk);
};
const onComplete = (full) => {
if (!window.isDestroyed()) window.webContents.send('chat-complete', full);
};
const onError = (err) => {
if (!window.isDestroyed()) window.webContents.send('chat-error', typeof err === 'string' ? err : err.message);
};
const onStatus = (status) => {
if (!window.isDestroyed()) window.webContents.send('chat-status', status);
};
if (isOllama) {
// Ensure key is loaded
const key = await getSecret('ollama-cloud-key');
ollamaApi.setApiKey(key);
ollamaApi.streamChat(messages, cleanModel, onChunk, onComplete, onError, onStatus);
} else {
qwenStreamChat(messages, model, onChunk, onComplete, onError, onStatus);
}
},
(fullResponse) => !window.isDestroyed() && window.webContents.send('chat-complete', fullResponse),
(error) => !window.isDestroyed() && window.webContents.send('chat-error', error.message),
(status) => !window.isDestroyed() && window.webContents.send('chat-status', status)
);
});
// Ollama Specific Handlers
ipcMain.handle('ollama-get-key-status', async () => {
const key = await getSecret('ollama-cloud-key');
return { hasKey: !!key };
});
ipcMain.handle('ollama-save-key', async (_, { key }) => {
await saveSecret('ollama-cloud-key', key);
ollamaApi.setApiKey(key);
return true;
});
ipcMain.handle('ollama-get-models', async () => {
const key = await getSecret('ollama-cloud-key');
ollamaApi.setApiKey(key);
return await ollamaApi.listModels();
});
// FS Handlers

View File

@@ -0,0 +1,147 @@
import fs from 'fs';
import path from 'path';
import https from 'https';
import os from 'os';
/**
* Ollama Cloud API Bridge for Goose Ultra
* Base URL: https://ollama.com/api
*/
// We'll manage key storage via main.js using keytar
let cachedApiKey = null;
export function setApiKey(key) {
cachedApiKey = key;
}
let activeRequest = null;
export function abortActiveChat() {
if (activeRequest) {
try {
activeRequest.destroy();
} catch (e) { }
activeRequest = null;
}
}
export async function streamChat(messages, model = 'gpt-oss:120b', onChunk, onComplete, onError, onStatus) {
abortActiveChat();
if (!cachedApiKey) {
onError(new Error('OLLAMA_CLOUD_KEY_MISSING: Please set your Ollama Cloud API Key in Settings.'));
return;
}
const log = (msg) => {
if (onStatus) onStatus(`[Ollama] ${msg}`);
};
const body = JSON.stringify({
model,
messages,
stream: true
});
const options = {
hostname: 'ollama.com',
port: 443,
path: '/api/chat',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${cachedApiKey}`,
'Content-Length': Buffer.byteLength(body)
}
};
log(`Connecting to ollama.com as ${model}...`);
const req = https.request(options, (res) => {
activeRequest = req;
let fullResponse = '';
if (res.statusCode !== 200) {
let errBody = '';
res.on('data', (c) => errBody += c.toString());
res.on('end', () => {
onError(new Error(`Ollama API Error ${res.statusCode}: ${errBody}`));
});
return;
}
res.setEncoding('utf8');
let buffer = '';
res.on('data', (chunk) => {
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (!line.trim()) continue;
try {
const parsed = JSON.parse(line);
const content = parsed.message?.content || '';
if (content) {
fullResponse += content;
onChunk(content);
}
if (parsed.done) {
// Request is done according to Ollama API
}
} catch (e) {
// Ignore malformed JSON chunks
}
}
});
res.on('end', () => {
onComplete(fullResponse);
});
});
req.on('error', (e) => {
onError(e);
});
req.setNoDelay(true);
req.write(body);
req.end();
}
/**
* Fetch available models from Ollama Cloud
*/
export async function listModels() {
if (!cachedApiKey) return [];
return new Promise((resolve, reject) => {
const options = {
hostname: 'ollama.com',
port: 443,
path: '/api/tags',
method: 'GET',
headers: {
'Authorization': `Bearer ${cachedApiKey}`
}
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', (c) => body += c.toString());
res.on('end', () => {
try {
const data = JSON.parse(body);
resolve(data.models || []);
} catch (e) {
resolve([]);
}
});
});
req.on('error', (e) => resolve([]));
req.end();
});
}

View File

@@ -91,5 +91,11 @@ contextBridge.exposeInMainWorld('electron', {
// Browser
openBrowser: (url) => ipcRenderer.invoke('vi-open-browser', { url })
},
// Ollama Cloud
ollama: {
getKeyStatus: () => ipcRenderer.invoke('ollama-get-key-status'),
saveKey: (key) => ipcRenderer.invoke('ollama-save-key', { key }),
getModels: () => ipcRenderer.invoke('ollama-get-models')
}
});

View File

@@ -1876,16 +1876,6 @@
"xmlbuilder": ">=11.0.1"
}
},
"node_modules/@types/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/responselike": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
@@ -1896,14 +1886,6 @@
"@types/node": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2140,85 +2122,6 @@
"node": ">= 10.0.0"
}
},
"node_modules/archiver": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"archiver-utils": "^2.1.0",
"async": "^3.2.4",
"buffer-crc32": "^0.2.1",
"readable-stream": "^3.6.0",
"readdir-glob": "^1.1.2",
"tar-stream": "^2.2.0",
"zip-stream": "^4.1.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/archiver-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"glob": "^7.1.4",
"graceful-fs": "^4.2.0",
"lazystream": "^1.0.0",
"lodash.defaults": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.union": "^4.6.0",
"normalize-path": "^3.0.0",
"readable-stream": "^2.0.0"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/archiver-utils/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/archiver-utils/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/archiver-utils/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2903,23 +2806,6 @@
"node": ">=0.10.0"
}
},
"node_modules/compress-commons": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"buffer-crc32": "^0.2.13",
"crc32-stream": "^4.0.2",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3051,35 +2937,6 @@
"buffer": "^5.1.0"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/crc32-stream": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"crc-32": "^1.2.0",
"readable-stream": "^3.4.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3095,13 +2952,6 @@
"node": ">= 8"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT",
"peer": true
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -3400,16 +3250,6 @@
"node": ">=8"
}
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"peer": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dotenv": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz",
@@ -3511,61 +3351,6 @@
"node": ">=14.0.0"
}
},
"node_modules/electron-builder-squirrel-windows": {
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz",
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"app-builder-lib": "24.13.3",
"archiver": "^5.3.1",
"builder-util": "24.13.1",
"fs-extra": "^10.1.0"
}
},
"node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/electron-builder-squirrel-windows/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/electron-builder/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -4924,56 +4709,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/lazystream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"readable-stream": "^2.0.5"
},
"engines": {
"node": ">= 0.6.3"
}
},
"node_modules/lazystream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/lazystream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lazystream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
@@ -4990,46 +4725,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -5070,19 +4765,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/marked": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/matcher": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
@@ -6083,17 +5765,6 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/monaco-editor": {
"version": "0.55.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"peer": true,
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
@@ -6186,17 +5857,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
@@ -6612,17 +6272,6 @@
"node": ">= 6"
}
},
"node_modules/readdir-glob": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"minimatch": "^5.1.0"
}
},
"node_modules/remark-gfm": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
@@ -7898,45 +7547,6 @@
"fd-slicer": "~1.1.0"
}
},
"node_modules/zip-stream": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"archiver-utils": "^3.0.4",
"compress-commons": "^4.1.2",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/zip-stream/node_modules/archiver-utils": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"glob": "^7.2.3",
"graceful-fs": "^4.2.0",
"lazystream": "^1.0.0",
"lodash.defaults": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.union": "^4.6.0",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View File

@@ -688,6 +688,114 @@ export const Sidebar = () => {
{state.executionSettings.localPowerShellEnabled ? 'ON' : 'OFF'}
</span>
</div>
{/* AI Models Manager */}
<div className="mt-4 pt-4 border-t border-white/5">
<div className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest mb-3 px-2 flex items-center gap-2">
<Icons.Sparkles className="w-3 h-3" />
AI Models
</div>
{/* Active Model Display */}
<div className="px-2 mb-2">
<div className="bg-white/5 border border-white/10 rounded-xl p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-[9px] text-zinc-500 uppercase tracking-widest">Active Model</span>
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded ${state.chatSettings.activeModel.startsWith('ollama:')
? 'bg-blue-500/20 text-blue-400'
: 'bg-emerald-500/20 text-emerald-400'
}`}>
{state.chatSettings.activeModel.startsWith('ollama:') ? 'OLLAMA' : 'QWEN'}
</span>
</div>
<div className="text-xs text-white font-mono truncate">
{state.chatSettings.activeModel}
</div>
</div>
</div>
{/* Qwen OAuth Status */}
<div
className="flex items-center gap-2 text-xs cursor-pointer transition-colors p-2 hover:bg-white/5 rounded-lg mx-2 group"
onClick={() => {
// Trigger Qwen OAuth flow
const electron = (window as any).electron;
if (electron?.openQwenAuth) {
electron.openQwenAuth();
}
}}
title="Click to authenticate with Qwen"
>
<div className="w-6 h-6 rounded-lg bg-emerald-500/20 flex items-center justify-center border border-emerald-500/30">
<span className="text-emerald-400 font-bold text-xs">Q</span>
</div>
<div className="flex-1 min-w-0">
<div className="text-zinc-300 text-xs font-medium">Qwen Cloud</div>
<div className="text-[9px] text-emerald-500">Connected Free Tier</div>
</div>
<Icons.CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
</div>
{/* Ollama Cloud Status */}
<div
className={`flex items-center gap-2 text-xs cursor-pointer transition-colors p-2 hover:bg-white/5 rounded-lg mx-2 group ${state.chatSettings.ollamaEnabled ? '' : 'opacity-60'
}`}
onClick={async () => {
// Check Ollama status and open settings if not configured
const electron = (window as any).electron;
if (electron?.ollama) {
const status = await electron.ollama.getKeyStatus();
if (!status.hasKey) {
// Open AI Settings modal - dispatch a custom event
window.dispatchEvent(new CustomEvent('open-ai-settings'));
}
}
}}
title={state.chatSettings.ollamaEnabled ? "Ollama Cloud connected" : "Click to configure Ollama Cloud"}
>
<div className={`w-6 h-6 rounded-lg flex items-center justify-center border ${state.chatSettings.ollamaEnabled
? 'bg-blue-500/20 border-blue-500/30'
: 'bg-zinc-800 border-white/10'
}`}>
<Icons.Cpu className={`w-3.5 h-3.5 ${state.chatSettings.ollamaEnabled ? 'text-blue-400' : 'text-zinc-500'}`} />
</div>
<div className="flex-1 min-w-0">
<div className="text-zinc-300 text-xs font-medium">Ollama Cloud</div>
<div className={`text-[9px] ${state.chatSettings.ollamaEnabled ? 'text-blue-400' : 'text-zinc-600'}`}>
{state.chatSettings.ollamaEnabled ? `${state.chatSettings.availableModels.filter(m => m.startsWith('ollama:')).length} models available` : 'Not configured'}
</div>
</div>
{state.chatSettings.ollamaEnabled ? (
<Icons.CheckCircle className="w-3.5 h-3.5 text-blue-400" />
) : (
<Icons.Plus className="w-3.5 h-3.5 text-zinc-500 group-hover:text-white transition-colors" />
)}
</div>
{/* Model Selector Dropdown */}
{state.chatSettings.availableModels.length > 1 && (
<div className="px-2 mt-3">
<select
value={state.chatSettings.activeModel}
onChange={(e) => dispatch({ type: 'SET_CHAT_MODEL', model: e.target.value })}
className="w-full bg-black/50 border border-white/10 rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-primary/50 cursor-pointer"
>
<optgroup label="Qwen Cloud">
{state.chatSettings.availableModels.filter(m => !m.startsWith('ollama:')).map(model => (
<option key={model} value={model}>{model}</option>
))}
</optgroup>
{state.chatSettings.availableModels.some(m => m.startsWith('ollama:')) && (
<optgroup label="Ollama Cloud">
{state.chatSettings.availableModels.filter(m => m.startsWith('ollama:')).map(model => (
<option key={model} value={model}>{model.replace('ollama:', '')}</option>
))}
</optgroup>
)}
</select>
</div>
)}
</div>
</div>
</div >
);
@@ -1625,6 +1733,308 @@ const SkillsSelectorModal = ({ onClose, onSelect }: { onClose: () => void, onSel
);
};
export // --- AI Settings Modal (Ollama Cloud & Model Control) ---
function AISettingsModal({ onClose }: { onClose: () => void }) {
const { state, dispatch } = useOrchestrator();
const [ollamaKey, setOllamaKey] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'qwen' | 'ollama'>('ollama');
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
// Official Ollama Cloud models from https://ollama.com/search?c=cloud
const FREE_OLLAMA_MODELS = [
// Top Tier - Most Popular
{ id: 'gpt-oss:120b', name: 'GPT-OSS 120B', category: 'Flagship', description: 'OpenAI\'s open-weight model for reasoning, agentic tasks', size: '120B', free: true },
{ id: 'deepseek-v3.2', name: 'DeepSeek V3.2', category: 'Flagship', description: 'High efficiency with superior reasoning and agent performance', size: 'MoE', free: true },
{ id: 'deepseek-v3.1:671b', name: 'DeepSeek V3.1', category: 'Flagship', description: 'Hybrid thinking/non-thinking mode', size: '671B', free: true },
{ id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro Preview', category: 'Flagship', description: 'Google\'s most intelligent model with SOTA reasoning', size: 'Cloud', free: true },
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', category: 'Fast', description: 'Frontier intelligence built for speed', size: 'Cloud', free: true },
// Coding Models
{ id: 'qwen3-coder:480b', name: 'Qwen3 Coder 480B', category: 'Coding', description: 'Alibaba\'s performant long context for agentic and coding', size: '480B', free: true },
{ id: 'qwen3-coder:30b', name: 'Qwen3 Coder 30B', category: 'Coding', description: 'Alibaba\'s agentic and coding model', size: '30B', free: true },
{ id: 'devstral-2:123b', name: 'Devstral 2 123B', category: 'Coding', description: 'Excels at codebase exploration and multi-file editing', size: '123B', free: true },
{ id: 'devstral-small-2:24b', name: 'Devstral Small 2 24B', category: 'Coding', description: 'Vision + tools for software engineering agents', size: '24B', free: true },
{ id: 'rnj-1:8b', name: 'RNJ-1 8B', category: 'Coding', description: 'Essential AI model optimized for code and STEM', size: '8B', free: true },
// Reasoning Models
{ id: 'qwen3-next:80b', name: 'Qwen3 Next 80B', category: 'Reasoning', description: 'Strong parameter efficiency and inference speed', size: '80B', free: true },
{ id: 'kimi-k2', name: 'Kimi K2', category: 'Reasoning', description: 'State-of-the-art MoE model for coding agent tasks', size: 'MoE', free: true },
{ id: 'kimi-k2-thinking', name: 'Kimi K2 Thinking', category: 'Reasoning', description: 'Moonshot AI\'s best open-source thinking model', size: 'MoE', free: true },
{ id: 'cogito-2.1:671b', name: 'Cogito 2.1', category: 'Reasoning', description: 'Instruction tuned generative model (MIT license)', size: '671B', free: true },
// Vision Models
{ id: 'qwen3-vl:235b', name: 'Qwen3 VL 235B', category: 'Vision', description: 'Most powerful vision-language model in Qwen family', size: '235B', free: true },
{ id: 'qwen3-vl:32b', name: 'Qwen3 VL 32B', category: 'Vision', description: 'Powerful vision-language understanding', size: '32B', free: true },
{ id: 'gemma3:27b', name: 'Gemma 3 27B', category: 'Vision', description: 'Most capable model that runs on a single GPU', size: '27B', free: true },
// Fast / Edge Models
{ id: 'ministral-3:14b', name: 'Ministral 3 14B', category: 'Fast', description: 'Designed for edge deployment', size: '14B', free: true },
{ id: 'ministral-3:8b', name: 'Ministral 3 8B', category: 'Fast', description: 'Edge deployment with vision + tools', size: '8B', free: true },
{ id: 'nemotron-3-nano', name: 'Nemotron 3 Nano', category: 'Fast', description: 'Efficient, open, and intelligent agentic model', size: 'Nano', free: true },
// Enterprise / Large Scale
{ id: 'glm-4.6', name: 'GLM 4.6', category: 'Flagship', description: 'Advanced agentic, reasoning and coding capabilities', size: 'Large', free: true },
{ id: 'minimax-m2', name: 'MiniMax M2', category: 'Flagship', description: 'High-efficiency LLM for coding and agentic workflows', size: 'Large', free: true },
{ id: 'mistral-large-3', name: 'Mistral Large 3', category: 'Flagship', description: 'Multimodal MoE for production-grade tasks', size: 'MoE', free: true },
];
const categories = ['all', ...Array.from(new Set(FREE_OLLAMA_MODELS.map(m => m.category)))];
const filteredModels = FREE_OLLAMA_MODELS.filter(m => {
const matchesSearch = m.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
m.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = selectedCategory === 'all' || m.category === selectedCategory;
return matchesSearch && matchesCategory;
});
const handleSelectOllamaModel = (modelId: string) => {
const fullModelId = `ollama:${modelId}`;
// Add to available models if not already there
if (!state.chatSettings.availableModels.includes(fullModelId)) {
dispatch({
type: 'SET_AVAILABLE_MODELS',
models: [...state.chatSettings.availableModels, fullModelId]
});
}
// Set as active model
dispatch({ type: 'SET_CHAT_MODEL', model: fullModelId });
dispatch({ type: 'TOGGLE_OLLAMA', enabled: true });
};
const handleSaveKey = async () => {
if (!ollamaKey.trim()) return;
setIsSaving(true);
setError(null);
try {
await (window as any).electron.ollama.saveKey(ollamaKey);
setOllamaKey('');
setError(null);
} catch (e: any) {
setError(e.message || "Failed to save key");
} finally {
setIsSaving(false);
}
};
return (
<div className="fixed inset-0 bg-black/80 backdrop-blur-md z-[100] flex items-center justify-center p-6 animate-fade-in">
<div className="bg-[#0f0f11] border border-white/10 rounded-3xl w-full max-w-2xl shadow-[0_0_100px_rgba(0,0,0,0.5)] overflow-hidden flex flex-col max-h-[85vh]">
{/* Header */}
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/5 shrink-0">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-xl">
<Icons.Sparkles className="w-5 h-5 text-primary" />
</div>
<div>
<h3 className="text-white font-bold text-lg">AI Model Manager</h3>
<p className="text-zinc-500 text-xs">Select your preferred AI model</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-white/10 rounded-lg text-zinc-400 hover:text-white transition-colors">
<Icons.X className="w-5 h-5" />
</button>
</div>
{/* Tab Navigation */}
<div className="flex border-b border-white/5 shrink-0">
<button
onClick={() => setActiveTab('qwen')}
className={`flex-1 py-3 text-sm font-bold transition-all flex items-center justify-center gap-2 ${activeTab === 'qwen'
? 'text-emerald-400 border-b-2 border-emerald-400 bg-emerald-500/5'
: 'text-zinc-500 hover:text-zinc-300'
}`}
>
<span className="w-5 h-5 rounded bg-emerald-500/20 flex items-center justify-center text-xs font-bold text-emerald-400">Q</span>
Qwen Cloud
</button>
<button
onClick={() => setActiveTab('ollama')}
className={`flex-1 py-3 text-sm font-bold transition-all flex items-center justify-center gap-2 ${activeTab === 'ollama'
? 'text-blue-400 border-b-2 border-blue-400 bg-blue-500/5'
: 'text-zinc-500 hover:text-zinc-300'
}`}
>
<Icons.Cpu className="w-4 h-4" />
Ollama Cloud
<span className="text-[9px] bg-blue-500/20 text-blue-400 px-1.5 py-0.5 rounded font-bold">FREE</span>
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto">
{activeTab === 'qwen' && (
<div className="p-6 space-y-4">
<div className="text-center py-8">
<div className="w-16 h-16 bg-emerald-500/20 rounded-2xl flex items-center justify-center mx-auto mb-4 border border-emerald-500/30">
<span className="text-emerald-400 font-bold text-2xl">Q</span>
</div>
<h4 className="text-white font-bold text-lg mb-2">Qwen Cloud</h4>
<p className="text-zinc-500 text-sm mb-6">Alibaba's powerful AI models with free tier access</p>
<div className="space-y-2 max-w-xs mx-auto">
{['qwen-coder-plus', 'qwen-plus', 'qwen-turbo'].map(model => (
<button
key={model}
onClick={() => dispatch({ type: 'SET_CHAT_MODEL', model })}
className={`w-full flex items-center justify-between p-4 rounded-xl border transition-all ${state.chatSettings.activeModel === model
? 'bg-emerald-500/10 border-emerald-500/40 text-white'
: 'bg-white/5 border-white/10 text-zinc-400 hover:border-white/20'
}`}
>
<span className="font-mono text-sm">{model}</span>
{state.chatSettings.activeModel === model && (
<Icons.Check className="w-4 h-4 text-emerald-400" />
)}
</button>
))}
</div>
</div>
</div>
)}
{activeTab === 'ollama' && (
<div className="p-4 space-y-4">
{/* API Key Section (Collapsible) */}
<details className="bg-white/5 border border-white/10 rounded-xl">
<summary className="p-4 cursor-pointer text-sm font-bold text-zinc-300 flex items-center justify-between">
<span className="flex items-center gap-2">
<Icons.Key className="w-4 h-4 text-zinc-500" />
API Key (Optional)
</span>
<Icons.ChevronDown className="w-4 h-4 text-zinc-500" />
</summary>
<div className="p-4 pt-0 space-y-3">
<div className="flex items-center justify-between">
<p className="text-[10px] text-zinc-500">For private models or higher rate limits. Many models work without a key.</p>
<a
href="https://ollama.com/settings/keys"
target="_blank"
rel="noopener noreferrer"
className="text-[10px] bg-blue-500/20 text-blue-400 px-2 py-1 rounded-lg font-bold hover:bg-blue-500/30 transition-colors flex items-center gap-1 shrink-0"
>
<Icons.ExternalLink className="w-3 h-3" />
Get Key
</a>
</div>
<div className="flex gap-2">
<input
type="password"
value={ollamaKey}
onChange={(e) => setOllamaKey(e.target.value)}
placeholder="sk-..."
className="flex-1 bg-black border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-primary/50"
/>
<button
onClick={handleSaveKey}
disabled={isSaving || !ollamaKey.trim()}
className="px-4 bg-white text-black font-bold rounded-lg hover:bg-zinc-200 transition-colors disabled:opacity-50 text-sm"
>
{isSaving ? '...' : 'Save'}
</button>
</div>
</div>
</details>
{/* Search and Filter */}
<div className="flex gap-2">
<div className="flex-1 relative">
<Icons.Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search models..."
className="w-full bg-black/50 border border-white/10 rounded-xl pl-10 pr-4 py-2.5 text-sm text-white focus:outline-none focus:border-primary/50"
/>
</div>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="bg-black/50 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-primary/50"
>
{categories.map(cat => (
<option key={cat} value={cat}>{cat === 'all' ? 'All Categories' : cat}</option>
))}
</select>
</div>
{/* Model Grid */}
<div className="grid grid-cols-1 gap-2">
{filteredModels.map(model => {
const isActive = state.chatSettings.activeModel === `ollama:${model.id}`;
return (
<button
key={model.id}
onClick={() => handleSelectOllamaModel(model.id)}
className={`flex items-center gap-4 p-4 rounded-xl border transition-all text-left group ${isActive
? 'bg-blue-500/10 border-blue-500/40 shadow-[0_0_20px_rgba(59,130,246,0.1)]'
: 'bg-white/5 border-white/5 hover:border-white/20 hover:bg-white/10'
}`}
>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${isActive ? 'bg-blue-500 text-white' : 'bg-zinc-800 text-zinc-400 group-hover:bg-zinc-700'
}`}>
{model.category === 'Coding' ? <Icons.Code className="w-5 h-5" /> :
model.category === 'Vision' ? <Icons.Eye className="w-5 h-5" /> :
model.category === 'Fast' ? <Icons.Zap className="w-5 h-5" /> :
model.category === 'Reasoning' ? <Icons.Brain className="w-5 h-5" /> :
<Icons.Sparkles className="w-5 h-5" />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span className={`font-bold text-sm ${isActive ? 'text-white' : 'text-zinc-300'}`}>{model.name}</span>
<span className="text-[9px] bg-zinc-800 text-zinc-500 px-1.5 py-0.5 rounded font-mono">{model.size}</span>
</div>
<p className="text-[11px] text-zinc-500 truncate">{model.description}</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className={`text-[9px] px-2 py-1 rounded-full font-bold ${model.category === 'Coding' ? 'bg-purple-500/20 text-purple-400' :
model.category === 'Vision' ? 'bg-amber-500/20 text-amber-400' :
model.category === 'Fast' ? 'bg-cyan-500/20 text-cyan-400' :
model.category === 'Reasoning' ? 'bg-pink-500/20 text-pink-400' :
'bg-blue-500/20 text-blue-400'
}`}>{model.category}</span>
{isActive && <Icons.Check className="w-5 h-5 text-blue-400" />}
</div>
</button>
);
})}
</div>
{filteredModels.length === 0 && (
<div className="text-center py-8 text-zinc-500">
<Icons.Search className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>No models found matching "{searchQuery}"</p>
</div>
)}
{error && <div className="text-xs text-rose-500 font-medium bg-rose-500/10 p-3 rounded-lg border border-rose-500/20">{error}</div>}
</div>
)}
</div>
{/* Footer */}
<div className="p-4 bg-white/5 border-t border-white/5 flex items-center justify-between shrink-0">
<div className="text-xs text-zinc-500 flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${state.chatSettings.activeModel.startsWith('ollama:') ? 'bg-blue-500' : 'bg-emerald-500'} animate-pulse`} />
Active: <span className="text-white font-mono">{state.chatSettings.activeModel}</span>
</div>
<button
onClick={onClose}
className="px-6 py-2.5 bg-white text-black font-bold rounded-xl hover:bg-zinc-200 transition-all"
>
Done
</button>
</div>
</div>
</div>
);
}
export const ChatPanel = () => {
const { state, dispatch } = useOrchestrator();
const [input, setInput] = useState('');
@@ -1635,6 +2045,8 @@ export const ChatPanel = () => {
const timelineScrollRef = React.useRef<HTMLDivElement | null>(null);
const streamingScrollRef = React.useRef<HTMLDivElement | null>(null);
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const [showAISettings, setShowAISettings] = useState(false);
const [showCustomPersona, setShowCustomPersona] = useState(false);
const [showSkillsSelector, setShowSkillsSelector] = useState(false);
const [customPersonaNameDraft, setCustomPersonaNameDraft] = useState(state.customChatPersonaName);
@@ -1649,6 +2061,13 @@ export const ChatPanel = () => {
const [recommendedSkills, setRecommendedSkills] = useState<Array<{ id: string; name: string; icon: string }>>([]);
const skillDebounceRef = React.useRef<NodeJS.Timeout | null>(null);
// Listen for AI Settings open event from sidebar
useEffect(() => {
const handleOpenAISettings = () => setShowAISettings(true);
window.addEventListener('open-ai-settings', handleOpenAISettings);
return () => window.removeEventListener('open-ai-settings', handleOpenAISettings);
}, []);
useEffect(() => {
if (skillDebounceRef.current) clearTimeout(skillDebounceRef.current);
if (input.length < 4) {
@@ -2282,7 +2701,7 @@ Brief plan starting with '[PLAN]'.`;
(window as any).electron.startChat([
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt }
], 'qwen-coder-plus');
], state.chatSettings.activeModel);
} else {
clearTimeout(timeoutId);
@@ -2457,12 +2876,22 @@ Brief plan starting with '[PLAN]'.`;
</span>
)}
<span className="text-[9px] px-1.5 py-0.5 bg-zinc-800 rounded text-zinc-500 uppercase tracking-wider border border-white/5">{state.state}</span>
<button
onClick={() => setShowAISettings(true)}
className="p-1.5 hover:bg-white/10 rounded-lg text-zinc-400 hover:text-white transition-colors"
title="AI Settings"
>
<Icons.Settings className="w-3.5 h-3.5" />
</button>
{state.chatDocked === 'bottom' && (
<button onClick={() => dispatch({ type: 'TOGGLE_CHAT_DOCK' })} className="hover:text-white text-zinc-500"><Icons.Layout className="w-3 h-3" /></button>
)}
</div>
</div>
{/* AI SETTINGS MODAL */}
{showAISettings && <AISettingsModal onClose={() => setShowAISettings(false)} />}
<div
ref={timelineScrollRef}
className="flex-1 overflow-y-auto p-4 space-y-6 scroll-smooth"

View File

@@ -213,8 +213,8 @@ export const StartView = () => {
(window as any).electron.startChat([
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
], 'qwen-coder-plus');
{ role: 'user', content: combinedPrompt }
], state.chatSettings.activeModel);
let streamBuffer = '';
(window as any).electron.onChatChunk((chunk: string) => {
@@ -955,7 +955,7 @@ export const PlanView = () => {
)}
{/* LAYER 3: Emergency / Abort buttons */}
{(state.state === OrchestratorState.Planning || state.state === OrchestratorState.Building) && (
{(state.state === OrchestratorState.Planning) && (
<button
onClick={() => {
if (confirm('Are you sure you want to cancel the CURRENT AI action?')) {

View File

@@ -59,6 +59,11 @@ export const INITIAL_CONTEXT: OrchestratorContext = {
// Settings
preferredFramework: null,
chatSettings: {
activeModel: 'qwen-coder-plus',
ollamaEnabled: false,
availableModels: ['qwen-coder-plus', 'qwen-plus', 'qwen-turbo']
},
// Apex Level PASS - Elite Developer Mode
apexModeEnabled: false
@@ -120,4 +125,5 @@ export const Icons = {
ZapOff: (props: any) => <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><polyline points="12.41 6.75 13 2 10.57 4.92" /><polyline points="18.57 12.91 21 10 15.66 10" /><polyline points="8 8 3 14 12 14 11 22 16 16" /><line x1="1" y1="1" x2="23" y2="23" /></svg>,
Minimize2: (props: any) => <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><polyline points="4 14 10 14 10 20" /><polyline points="20 10 14 10 14 4" /><line x1="14" y1="10" x2="21" y2="3" /><line x1="3" y1="21" x2="10" y2="14" /></svg>,
Maximize2: (props: any) => <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><polyline points="15 3 21 3 21 9" /><polyline points="9 21 3 21 3 15" /><line x1="21" y1="3" x2="14" y2="10" /><line x1="3" y1="21" x2="10" y2="14" /></svg>,
Brain: (props: any) => <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" /><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" /><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" /><path d="M17.599 6.5a3 3 0 0 0 .399-1.375" /><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5" /><path d="M3.477 10.896a4 4 0 0 1 .585-.396" /><path d="M19.938 10.5a4 4 0 0 1 .585.396" /><path d="M6 18a4 4 0 0 1-1.967-.516" /><path d="M19.967 17.484A4 4 0 0 1 18 18" /></svg>,
};

View File

@@ -72,7 +72,12 @@ type Action =
| { type: 'SET_PREFERRED_FRAMEWORK'; framework: string | null }
| { type: 'SET_STATE'; state: OrchestratorState }
// Apex Level PASS
| { type: 'TOGGLE_APEX_MODE' };
// Apex Level PASS
| { type: 'TOGGLE_APEX_MODE' }
// Chat Settings
| { type: 'SET_CHAT_MODEL'; model: string }
| { type: 'TOGGLE_OLLAMA'; enabled: boolean }
| { type: 'SET_AVAILABLE_MODELS'; models: string[] };
// --- Helper: Tab Eligibility ---
// Strictly enforces "Tab validity" rule
@@ -377,6 +382,15 @@ const reducer = (state: OrchestratorContext, action: Action): OrchestratorContex
case 'TOGGLE_APEX_MODE':
return { ...state, apexModeEnabled: !state.apexModeEnabled };
case 'SET_CHAT_MODEL':
return { ...state, chatSettings: { ...state.chatSettings, activeModel: action.model } };
case 'TOGGLE_OLLAMA':
return { ...state, chatSettings: { ...state.chatSettings, ollamaEnabled: action.enabled } };
case 'SET_AVAILABLE_MODELS':
return { ...state, chatSettings: { ...state.chatSettings, availableModels: action.models } };
default:
return state;
}

View File

@@ -178,6 +178,11 @@ export interface OrchestratorContext {
// Settings
preferredFramework: string | null;
chatSettings: {
activeModel: string;
ollamaEnabled: boolean;
availableModels: string[];
};
// Apex Level PASS - Elite Developer Mode
apexModeEnabled: boolean;