@pheem49/mint 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.codex +0 -0
  2. package/README.md +174 -126
  3. package/main.js +21 -1
  4. package/mint-cli-logic.js +21 -1
  5. package/mint-cli.js +287 -45
  6. package/package.json +13 -2
  7. package/src/AI_Brain/Gemini_API.js +331 -64
  8. package/src/AI_Brain/agent_orchestrator.js +73 -0
  9. package/src/AI_Brain/autonomous_brain.js +2 -0
  10. package/src/AI_Brain/memory_store.js +318 -0
  11. package/src/AI_Brain/proactive_engine.js +2 -8
  12. package/src/Automation_Layer/file_operations.js +123 -4
  13. package/src/Automation_Layer/open_app.js +72 -43
  14. package/src/Automation_Layer/open_website.js +3 -3
  15. package/src/CLI/chat_router.js +57 -9
  16. package/src/CLI/chat_ui.js +117 -11
  17. package/src/CLI/code_agent.js +249 -36
  18. package/src/CLI/onboarding.js +53 -6
  19. package/src/CLI/workspace_manager.js +90 -0
  20. package/src/Plugins/docker.js +12 -10
  21. package/src/Plugins/spotify.js +168 -40
  22. package/src/Plugins/system_monitor.js +72 -0
  23. package/src/System/config_manager.js +35 -2
  24. package/src/System/custom_workflows.js +9 -2
  25. package/src/System/notifications.js +23 -0
  26. package/src/UI/settings.html +143 -65
  27. package/src/UI/settings.js +155 -41
  28. package/tests/agent_orchestrator.test.js +41 -0
  29. package/tests/chat_router.test.js +42 -0
  30. package/tests/code_agent.test.js +69 -0
  31. package/tests/config_manager.test.js +141 -0
  32. package/tests/docker.test.js +46 -0
  33. package/tests/file_operations.test.js +57 -0
  34. package/tests/memory_store.test.js +185 -0
  35. package/tests/provider_routing.test.js +67 -0
  36. package/tests/spotify.test.js +201 -0
  37. package/tests/system_monitor.test.js +37 -0
  38. package/tests/workspace_manager.test.js +56 -0
@@ -1,45 +1,173 @@
1
- const { exec } = require('child_process');
1
+ /**
2
+ * Mint Spotify Plugin — Complete Edition
3
+ * ----------------------------------------
4
+ * Controls Spotify playback via playerctl (no OAuth required).
5
+ * Supports: play, pause, next, previous, stop, shuffle, volume,
6
+ * now_playing, search (opens Spotify search URL).
7
+ *
8
+ * Requirements: playerctl installed (sudo apt install playerctl)
9
+ * Spotify must be running (Desktop app or Snap).
10
+ */
11
+
12
+ const { exec, execSync } = require('child_process');
13
+ const { promisify } = require('util');
14
+ const execAsync = promisify(exec);
15
+
16
+ // ── Helpers ────────────────────────────────────────────────────────────────
17
+
18
+ async function runPlayerctl(args) {
19
+ try {
20
+ const { stdout } = await execAsync(`playerctl -p spotify ${args}`);
21
+ return { ok: true, output: stdout.trim() };
22
+ } catch (err) {
23
+ const msg = (err.stderr || err.message || '').toLowerCase();
24
+ if (msg.includes('no players found') || msg.includes('could not find player')) {
25
+ return { ok: false, error: 'spotify_not_running' };
26
+ }
27
+ if (err.code === 127) {
28
+ return { ok: false, error: 'playerctl_missing' };
29
+ }
30
+ return { ok: false, error: err.message };
31
+ }
32
+ }
33
+
34
+ function formatError(errorCode) {
35
+ if (errorCode === 'spotify_not_running') {
36
+ return '🎵 Spotify ยังไม่ได้เปิดอยู่นะคะ กรุณาเปิด Spotify ก่อนนะคะ';
37
+ }
38
+ if (errorCode === 'playerctl_missing') {
39
+ return '⚠️ ไม่พบ playerctl กรุณาติดตั้งด้วยคำสั่ง: sudo apt install playerctl';
40
+ }
41
+ return `❌ เกิดข้อผิดพลาด: ${errorCode}`;
42
+ }
43
+
44
+ // ── Action Handlers ────────────────────────────────────────────────────────
45
+
46
+ const ACTION_MAP = {
47
+ 'play': () => runPlayerctl('play'),
48
+ 'pause': () => runPlayerctl('pause'),
49
+ 'stop': () => runPlayerctl('stop'),
50
+ 'next': () => runPlayerctl('next'),
51
+ 'previous': () => runPlayerctl('previous'),
52
+ 'prev': () => runPlayerctl('previous'),
53
+ };
54
+
55
+ const ACTION_MESSAGES = {
56
+ 'play': '▶️ เล่น Spotify แล้วค่ะ 🎵',
57
+ 'pause': '⏸️ หยุดเพลงชั่วคราวแล้วค่ะ',
58
+ 'stop': '⏹️ หยุด Spotify แล้วค่ะ',
59
+ 'next': '⏭️ ข้ามไปเพลงถัดไปแล้วค่ะ 🎵',
60
+ 'previous': '⏮️ กลับไปเพลงก่อนหน้าแล้วค่ะ',
61
+ 'prev': '⏮️ กลับไปเพลงก่อนหน้าแล้วค่ะ',
62
+ };
63
+
64
+ async function getNowPlaying() {
65
+ const [title, artist, album, status] = await Promise.all([
66
+ runPlayerctl('metadata title'),
67
+ runPlayerctl('metadata artist'),
68
+ runPlayerctl('metadata album'),
69
+ runPlayerctl('status'),
70
+ ]);
71
+
72
+ if (!title.ok) return formatError(title.error);
73
+
74
+ const statusIcon = (status.output || '').toLowerCase() === 'playing' ? '▶️' : '⏸️';
75
+ const titleText = title.output || 'ไม่ทราบชื่อเพลง';
76
+ const artistText = artist.output || 'ไม่ทราบศิลปิน';
77
+ const albumText = album.output || '';
78
+
79
+ let reply = `${statusIcon} กำลังเล่น: **${titleText}**\n`;
80
+ reply += `🎤 ศิลปิน: ${artistText}`;
81
+ if (albumText) reply += `\n💿 อัลบั้ม: ${albumText}`;
82
+ return reply;
83
+ }
84
+
85
+ async function setVolume(levelStr) {
86
+ const level = parseInt(levelStr, 10);
87
+ if (isNaN(level) || level < 0 || level > 100) {
88
+ return '⚠️ กรุณาระบุระดับเสียง 0-100 ค่ะ เช่น "volume 70"';
89
+ }
90
+ // playerctl volume uses 0.0–1.0
91
+ const result = await runPlayerctl(`volume ${(level / 100).toFixed(2)}`);
92
+ if (!result.ok) return formatError(result.error);
93
+ return `🔊 ปรับเสียงเป็น ${level}% แล้วค่ะ`;
94
+ }
95
+
96
+ async function setShuffle(state) {
97
+ // state: 'on' | 'off' | 'toggle'
98
+ const shuffleState = state === 'on' ? 'On' : state === 'off' ? 'Off' : 'Toggle';
99
+ const result = await runPlayerctl(`shuffle ${shuffleState}`);
100
+ if (!result.ok) return formatError(result.error);
101
+ if (state === 'toggle') return '🔀 สลับโหมด Shuffle แล้วค่ะ';
102
+ return `🔀 Shuffle ${state === 'on' ? 'เปิด' : 'ปิด'}แล้วค่ะ`;
103
+ }
104
+
105
+ function searchSpotify(query) {
106
+ if (!query || !query.trim()) {
107
+ return '⚠️ กรุณาระบุคำที่ต้องการค้นหาด้วยนะคะ เช่น "search BTS"';
108
+ }
109
+ const encoded = encodeURIComponent(query.trim());
110
+ const url = `https://open.spotify.com/search/${encoded}`;
111
+ try {
112
+ const { exec: execSync2 } = require('child_process');
113
+ execSync2(`xdg-open "${url}"`, { detached: true, stdio: 'ignore' });
114
+ return `🔍 เปิดค้นหา "${query}" ใน Spotify แล้วค่ะ 🎵`;
115
+ } catch (_) {
116
+ return `🔍 ค้นหา "${query}" ที่: ${url}`;
117
+ }
118
+ }
119
+
120
+ // ── Main Plugin Export ─────────────────────────────────────────────────────
2
121
 
3
122
  module.exports = {
4
123
  name: 'spotify',
5
- description: 'Controls Spotify playback (play, pause, next, previous). Only works if Spotify is running. Valid targets are: "play", "pause", "next", "previous".',
6
-
124
+ description: [
125
+ 'Controls Spotify playback and gets now-playing info.',
126
+ 'Valid targets:',
127
+ ' "play" | "pause" | "stop" | "next" | "previous" — playback control',
128
+ ' "now_playing" or "status" — get current song info',
129
+ ' "volume <0-100>" — set volume level (e.g. "volume 70")',
130
+ ' "shuffle on" | "shuffle off" | "shuffle toggle" — toggle shuffle',
131
+ ' "search <query>" — search Spotify (e.g. "search BTS Dynamite")',
132
+ ].join(' '),
133
+
7
134
  async execute(target) {
8
- return new Promise((resolve) => {
9
- const commandMap = {
10
- 'play': 'playerctl -p spotify play',
11
- 'pause': 'playerctl -p spotify pause',
12
- 'next': 'playerctl -p spotify next',
13
- 'previous': 'playerctl -p spotify previous'
14
- };
15
-
16
- const cmd = commandMap[target.toLowerCase()];
17
-
18
- if (!cmd) {
19
- return resolve(`Invalid spotify command: ${target}`);
20
- }
21
-
22
- exec(cmd, (error) => {
23
- if (error) {
24
- // Check if playerctl is missing or Spotify isn't running
25
- if (error.message.includes('No players found')) {
26
- return resolve('Spotify is not currently running or playing anything.');
27
- }
28
- if (error.code === 127) {
29
- return resolve('Error: "playerctl" is not installed on this system. Please install it (e.g., sudo apt install playerctl).');
30
- }
31
- return resolve(`Failed to execute Spotify command: ${error.message}`);
32
- }
33
-
34
- const actionText = {
35
- 'play': 'Playing Spotify.',
36
- 'pause': 'Paused Spotify.',
37
- 'next': 'Skipped to the next song.',
38
- 'previous': 'Went back to the previous song.'
39
- };
40
-
41
- resolve(actionText[target.toLowerCase()]);
42
- });
43
- });
44
- }
135
+ const raw = (target || '').trim().toLowerCase();
136
+
137
+ // ── Basic playback commands ───────────────────────────────────────
138
+ if (ACTION_MAP[raw]) {
139
+ const result = await ACTION_MAP[raw]();
140
+ if (!result.ok) return formatError(result.error);
141
+ return ACTION_MESSAGES[raw];
142
+ }
143
+
144
+ // ── Now Playing ───────────────────────────────────────────────────
145
+ if (raw === 'now_playing' || raw === 'status' || raw === 'what\'s playing' || raw === 'current') {
146
+ return await getNowPlaying();
147
+ }
148
+
149
+ // ── Volume ────────────────────────────────────────────────────────
150
+ if (raw.startsWith('volume')) {
151
+ const levelStr = raw.replace('volume', '').trim();
152
+ return await setVolume(levelStr);
153
+ }
154
+
155
+ // ── Shuffle ───────────────────────────────────────────────────────
156
+ if (raw.startsWith('shuffle')) {
157
+ const state = raw.replace('shuffle', '').trim() || 'toggle';
158
+ return await setShuffle(state);
159
+ }
160
+
161
+ // ── Search ────────────────────────────────────────────────────────
162
+ if (raw.startsWith('search')) {
163
+ const query = target.replace(/^search\s*/i, '').trim();
164
+ return searchSpotify(query);
165
+ }
166
+
167
+ // ── Fallback: try as playerctl arg directly ───────────────────────
168
+ return `⚠️ ไม่รู้จักคำสั่ง Spotify: "${target}"\nคำสั่งที่รองรับ: play, pause, stop, next, previous, now_playing, volume <0-100>, shuffle on/off, search <query>`;
169
+ },
170
+
171
+ // Expose helpers for testing
172
+ _helpers: { runPlayerctl, getNowPlaying, setVolume, setShuffle, searchSpotify }
45
173
  };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Mint System Monitor Plugin
3
+ * --------------------------
4
+ * Provides real-time system statistics for the host machine.
5
+ * Uses standard Linux commands (uptime, free, df) for lightweight monitoring.
6
+ */
7
+
8
+ const { exec } = require('child_process');
9
+ const { promisify } = require('util');
10
+ const execAsync = promisify(exec);
11
+ const os = require('os');
12
+
13
+ async function getStats() {
14
+ try {
15
+ const [uptime, free, df] = await Promise.all([
16
+ execAsync('uptime -p'),
17
+ execAsync('free -h'),
18
+ execAsync('df -h / --output=pcent,avail')
19
+ ]);
20
+
21
+ // Parse Memory
22
+ const memLines = free.stdout.split('\n');
23
+ const memLine = memLines.find(l => l.startsWith('Mem:')) || '';
24
+ const memParts = memLine.split(/\s+/).filter(Boolean);
25
+ const memUsed = memParts[2] || 'Unknown';
26
+ const memTotal = memParts[1] || 'Unknown';
27
+
28
+ // Parse Disk
29
+ const diskLines = df.stdout.trim().split('\n');
30
+ const diskLine = diskLines[1] || '';
31
+ const [diskPercent, diskAvail] = diskLine.trim().split(/\s+/);
32
+
33
+ const cpuLoad = os.loadavg()[0].toFixed(2);
34
+ const cpuCores = os.cpus().length;
35
+
36
+ let report = `📊 **System Health Report**\n`;
37
+ report += `⏱️ **Uptime:** ${uptime.stdout.trim()}\n`;
38
+ report += `💻 **CPU Load:** ${cpuLoad} (on ${cpuCores} cores)\n`;
39
+ report += `🧠 **Memory:** ${memUsed} / ${memTotal} used\n`;
40
+ report += `💽 **Disk (/):** ${diskAvail} available (${diskPercent} full)`;
41
+
42
+ return report;
43
+ } catch (err) {
44
+ return `❌ Error fetching system stats: ${err.message}`;
45
+ }
46
+ }
47
+
48
+ module.exports = {
49
+ name: 'system_monitor',
50
+ description: 'Provides system statistics like CPU load, memory usage, disk space, and uptime. Target can be "stats", "cpu", "memory", or "disk".',
51
+
52
+ async execute(target) {
53
+ const cmd = (target || 'stats').toLowerCase().trim();
54
+
55
+ switch (cmd) {
56
+ case 'stats':
57
+ case 'health':
58
+ return await getStats();
59
+ case 'cpu':
60
+ return `💻 **CPU Load (1m):** ${os.loadavg()[0].toFixed(2)}\nCores: ${os.cpus().length}\nModel: ${os.cpus()[0].model}`;
61
+ case 'memory':
62
+ case 'ram':
63
+ const { stdout: mem } = await execAsync('free -h');
64
+ return `🧠 **Memory Status:**\n\`\`\`\n${mem}\`\`\``;
65
+ case 'disk':
66
+ const { stdout: disk } = await execAsync('df -h /');
67
+ return `💽 **Disk Status:**\n\`\`\`\n${disk}\`\`\``;
68
+ default:
69
+ return await getStats();
70
+ }
71
+ }
72
+ };
@@ -60,8 +60,14 @@ const DEFAULT_CONFIG = {
60
60
  mcpServers: {},
61
61
  anthropicApiKey: '',
62
62
  openaiApiKey: '',
63
+ hfApiKey: '',
63
64
  anthropicModel: 'claude-3-5-sonnet-latest',
64
- openaiModel: 'gpt-4o'
65
+ openaiModel: 'gpt-4o',
66
+ hfModel: 'meta-llama/Meta-Llama-3-8B-Instruct',
67
+ localApiBaseUrl: 'http://localhost:1234/v1',
68
+ localModelName: 'local-model',
69
+ ollamaHost: 'http://localhost:11434',
70
+ enableAgentCollaboration: true
65
71
  };
66
72
 
67
73
 
@@ -90,4 +96,31 @@ function writeConfig(config) {
90
96
  }
91
97
  }
92
98
 
93
- module.exports = { readConfig, writeConfig, CONFIG_PATH };
99
+ function getAvailableProviders(config) {
100
+ const providers = [];
101
+ const cfg = config || readConfig();
102
+
103
+ const isPlaceholder = (val) => !val || val.startsWith('your_') || val.includes('key_here') || val.trim() === '';
104
+
105
+ // Check which providers have API keys or URLs configured
106
+ const anthropicKey = cfg.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
107
+ if (!isPlaceholder(anthropicKey)) providers.push('anthropic');
108
+
109
+ const openaiKey = cfg.openaiApiKey || process.env.OPENAI_API_KEY;
110
+ if (!isPlaceholder(openaiKey)) providers.push('openai');
111
+
112
+ const geminiKey = cfg.apiKey || process.env.GEMINI_API_KEY;
113
+ if (!isPlaceholder(geminiKey)) providers.push('gemini');
114
+
115
+ const hfKey = cfg.hfApiKey || process.env.HF_API_KEY;
116
+ if (!isPlaceholder(hfKey)) providers.push('huggingface');
117
+
118
+ if (cfg.localApiBaseUrl && cfg.localApiBaseUrl.trim() !== '') providers.push('local_openai');
119
+
120
+ // Always push ollama at the end since it's local
121
+ providers.push('ollama');
122
+
123
+ return providers;
124
+ }
125
+
126
+ module.exports = { readConfig, writeConfig, getAvailableProviders, CONFIG_PATH };
@@ -3,6 +3,10 @@ const path = require('path');
3
3
  const { app, shell } = require('electron');
4
4
  const { exec } = require('child_process');
5
5
 
6
+ function escapeRegExp(text) {
7
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8
+ }
9
+
6
10
  class CustomWorkflows {
7
11
  constructor() {
8
12
  this.configPath = path.join(app.getPath('userData'), 'workflows.json');
@@ -86,7 +90,7 @@ class CustomWorkflows {
86
90
  if (wf.trigger && wf.trigger.type === 'process_running' && wf.trigger.processName) {
87
91
  const targetName = wf.trigger.processName.toLowerCase();
88
92
  // simplistic exact-word match to avoid partial matches
89
- const regex = new RegExp(`^${targetName}$`, 'm');
93
+ const regex = new RegExp(`^${escapeRegExp(targetName)}$`, 'm');
90
94
  const isRunning = regex.test(runningProcesses);
91
95
 
92
96
  if (isRunning) {
@@ -124,4 +128,7 @@ class CustomWorkflows {
124
128
  }
125
129
  }
126
130
 
127
- module.exports = new CustomWorkflows();
131
+ const instance = new CustomWorkflows();
132
+ instance._helpers = { escapeRegExp };
133
+
134
+ module.exports = instance;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Mint Notification System
3
+ * ------------------------
4
+ * Sends system-level notifications to the user.
5
+ * Supports Linux (notify-send) as a primary target for CLI.
6
+ */
7
+
8
+ const { exec } = require('child_process');
9
+
10
+ function sendNotification(title, message, urgency = 'normal') {
11
+ // Attempt to use notify-send (Linux)
12
+ const cmd = `notify-send -u ${urgency} "${title}" "${message}"`;
13
+ exec(cmd, (err) => {
14
+ if (err) {
15
+ // Fallback: Silent console log if no notifier found
16
+ console.log(`[Notification] ${title}: ${message}`);
17
+ }
18
+ });
19
+ }
20
+
21
+ module.exports = {
22
+ sendNotification
23
+ };
@@ -38,74 +38,138 @@
38
38
  <div class="settings-content" id="settings-content">
39
39
 
40
40
  <div class="tab-pane active" id="sect-general">
41
- <!-- API Key -->
42
- <section class="setting-section">
43
- <h2 class="section-title">🔑 Gemini API Key</h2>
44
- <div class="setting-row">
45
- <label for="api-key-input">API Key</label>
46
- <div class="input-group">
47
- <input type="password" id="api-key-input" placeholder="Enter your Gemini API Key..."
48
- autocomplete="off">
49
- <button class="toggle-visibility" id="toggle-key" title="Show/Hide">👁</button>
50
- </div>
51
- <div class="api-actions">
52
- <button class="api-save-btn" id="save-api-key">Save API Key</button>
53
- <span class="api-save-status" id="api-save-status" aria-live="polite"></span>
54
- </div>
55
- <p class="hint">Get your API Key at <a href="#" id="ai-studio-link">Google AI Studio</a></p>
56
- </div>
57
- <div class="setting-row" style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; padding: 12px; background: rgba(139, 92, 246, 0.1); border-radius: 12px; border: 1px solid rgba(139, 92, 246, 0.2);">
58
- <div style="display: flex; flex-direction: column; gap: 4px;">
59
- <label for="show-desktop-widget" style="margin-bottom: 0; font-weight: 500;">Show Desktop AI Candidate</label>
60
- <span class="hint" style="font-size: 0.75rem;">Show the mini AI character on your desktop</span>
61
- </div>
62
- <label class="toggle-switch">
63
- <input type="checkbox" id="show-desktop-widget">
64
- <span class="toggle-slider"></span>
65
- </label>
66
- </div>
67
- </section>
68
- <!-- AI Provider & Model -->
69
- <section class="setting-section">
70
- <h2 class="section-title">🧠 AI Engine</h2>
71
- <div class="setting-row">
72
- <label for="ai-provider-select">Provider</label>
73
- <div class="input-group">
74
- <select id="ai-provider-select" style="width: 100%; background: var(--input-bg); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; color: var(--text-main); font-family: inherit; font-size: 0.9rem; outline: none; transition: border-color 0.2s; cursor: pointer;">
75
- <option value="gemini" style="background: var(--bg-color)">Google Gemini (Cloud)</option>
76
- <option value="ollama" style="background: var(--bg-color)">Ollama (Local / Private)</option>
77
- </select>
78
- </div>
79
- </div>
41
+ <!-- API Keys & Hosts -->
42
+ <section class="setting-section">
43
+ <h2 class="section-title">🔑 API Keys & Hosts</h2>
44
+ <div class="setting-row">
45
+ <label for="api-key-input">Gemini API Key</label>
46
+ <div class="input-group">
47
+ <input type="password" id="api-key-input" placeholder="Enter Gemini API Key..." autocomplete="off">
48
+ <button class="toggle-visibility" onclick="const i = document.getElementById('api-key-input'); i.type = i.type === 'password' ? 'text' : 'password';" title="Show/Hide">👁</button>
49
+ </div>
50
+ </div>
51
+ <div class="setting-row" style="margin-top: 10px;">
52
+ <label for="openai-api-key-input">OpenAI API Key</label>
53
+ <div class="input-group">
54
+ <input type="password" id="openai-api-key-input" placeholder="Enter OpenAI API Key..." autocomplete="off">
55
+ <button class="toggle-visibility" onclick="const i = document.getElementById('openai-api-key-input'); i.type = i.type === 'password' ? 'text' : 'password';" title="Show/Hide">👁</button>
56
+ </div>
57
+ </div>
58
+ <div class="setting-row" style="margin-top: 10px;">
59
+ <label for="anthropic-api-key-input">Anthropic API Key</label>
60
+ <div class="input-group">
61
+ <input type="password" id="anthropic-api-key-input" placeholder="Enter Anthropic API Key..." autocomplete="off">
62
+ <button class="toggle-visibility" onclick="const i = document.getElementById('anthropic-api-key-input'); i.type = i.type === 'password' ? 'text' : 'password';" title="Show/Hide">👁</button>
63
+ </div>
64
+ </div>
65
+ <div class="setting-row" style="margin-top: 10px;">
66
+ <label for="hf-api-key">Hugging Face API Key</label>
67
+ <div class="input-group">
68
+ <input type="password" id="hf-api-key" placeholder="Enter Hugging Face API Key..." autocomplete="off">
69
+ <button class="toggle-visibility" onclick="const i = document.getElementById('hf-api-key'); i.type = i.type === 'password' ? 'text' : 'password';" title="Show/Hide">👁</button>
70
+ </div>
71
+ </div>
72
+ <div class="setting-row" style="margin-top: 10px;">
73
+ <label for="local-api-base-url">LM Studio Base URL</label>
74
+ <input type="text" id="local-api-base-url" placeholder="e.g. http://localhost:1234/v1">
75
+ </div>
76
+ <div class="setting-row" style="margin-top: 10px;">
77
+ <label for="ollama-host-input">Ollama Host</label>
78
+ <input type="text" id="ollama-host-input" placeholder="e.g. http://localhost:11434">
79
+ </div>
80
+ <div class="setting-row" style="display: flex; align-items: center; justify-content: space-between; margin-top: 20px; padding: 12px; background: rgba(139, 92, 246, 0.1); border-radius: 12px; border: 1px solid rgba(139, 92, 246, 0.2);">
81
+ <div style="display: flex; flex-direction: column; gap: 4px;">
82
+ <label for="show-desktop-widget" style="margin-bottom: 0; font-weight: 500;">Show Desktop AI Candidate</label>
83
+ <span class="hint" style="font-size: 0.75rem;">Show the mini AI character on your desktop</span>
84
+ </div>
85
+ <label class="toggle-switch">
86
+ <input type="checkbox" id="show-desktop-widget">
87
+ <span class="toggle-slider"></span>
88
+ </label>
89
+ </div>
90
+ </section>
80
91
 
81
- <!-- Gemini Options -->
82
- <div id="gemini-options" style="margin-top: 14px; padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: rgba(0,0,0,0.1);">
83
- <div class="setting-row">
84
- <label for="gemini-model-select">Gemini Model</label>
85
- <div class="input-group">
86
- <select id="gemini-model-select" style="width: 100%; background: var(--input-bg); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; color: var(--text-main); font-family: inherit; font-size: 0.9rem; outline: none; transition: border-color 0.2s; cursor: pointer;">
87
- <option value="gemini-2.5-flash" style="background: var(--bg-color)">gemini-2.5-flash</option>
88
- <option value="gemini-3.1-flash-lite" style="background: var(--bg-color)">gemini-3.1-flash-lite</option>
89
- <option value="gemini-3.1-flash-lite-preview" style="background: var(--bg-color)">gemini-3.1-flash-lite-preview</option>
90
- <option value="custom" style="background: var(--bg-color)">Custom...</option>
91
- </select>
92
+ <!-- Models Configuration -->
93
+ <section class="setting-section">
94
+ <h2 class="section-title">🧠 AI Engine & Default Models</h2>
95
+ <div class="setting-row">
96
+ <label for="ai-provider-select">Active Provider</label>
97
+ <div class="input-group">
98
+ <select id="ai-provider-select" style="width: 100%; background: var(--input-bg); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; color: var(--text-main); font-family: inherit; font-size: 0.9rem; outline: none; transition: border-color 0.2s; cursor: pointer;">
99
+ <option value="gemini" style="background: var(--bg-color)">Google Gemini (Cloud)</option>
100
+ <option value="anthropic" style="background: var(--bg-color)">Anthropic Claude</option>
101
+ <option value="openai" style="background: var(--bg-color)">OpenAI</option>
102
+ <option value="ollama" style="background: var(--bg-color)">Ollama (Local / Private)</option>
103
+ <option value="huggingface" style="background: var(--bg-color)">Hugging Face (Inference API)</option>
104
+ <option value="local_openai" style="background: var(--bg-color)">Local (LM Studio / OpenAI Compatible)</option>
105
+ </select>
106
+ </div>
92
107
  </div>
93
- </div>
94
- <div class="setting-row" id="gemini-model-custom-row" style="display: none; margin-top: 10px;">
95
- <label for="gemini-model-custom">Custom Model</label>
96
- <input type="text" id="gemini-model-custom" placeholder="e.g. gemini-3.1-flash-lite-preview">
97
- </div>
98
- </div>
99
108
 
100
- <!-- Ollama Options -->
101
- <div id="ollama-options" style="display: none; margin-top: 14px; padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: rgba(0,0,0,0.1);">
102
- <div class="setting-row">
103
- <label for="ollama-model-input">Ollama Model</label>
104
- <input type="text" id="ollama-model-input" placeholder="e.g. llama3:latest">
105
- <p class="hint" style="margin-top: 8px; color: #fbbf24;">Make sure Ollama is running at http://localhost:11434</p>
106
- </div>
107
- </div>
108
- </section>
109
+ <div class="setting-row" style="margin-top: 14px;">
110
+ <label for="gemini-model-select">Gemini Model</label>
111
+ <div class="input-group">
112
+ <select id="gemini-model-select" style="width: 100%; background: var(--input-bg); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; color: var(--text-main); font-family: inherit; font-size: 0.9rem; outline: none; transition: border-color 0.2s; cursor: pointer;">
113
+ <option value="gemini-2.5-flash" style="background: var(--bg-color)">gemini-2.5-flash</option>
114
+ <option value="gemini-3.1-flash-lite" style="background: var(--bg-color)">gemini-3.1-flash-lite</option>
115
+ <option value="gemini-3.1-flash-lite-preview" style="background: var(--bg-color)">gemini-3.1-flash-lite-preview</option>
116
+ <option value="custom" style="background: var(--bg-color)">Custom...</option>
117
+ </select>
118
+ </div>
119
+ </div>
120
+ <div class="setting-row" id="gemini-model-custom-row" style="display: none; margin-top: 10px;">
121
+ <label for="gemini-model-custom">Custom Gemini Model</label>
122
+ <input type="text" id="gemini-model-custom" placeholder="e.g. gemini-3.1-flash-lite-preview">
123
+ </div>
124
+
125
+ <div class="setting-row" style="margin-top: 14px;">
126
+ <label for="openai-model-select">OpenAI Model</label>
127
+ <div class="input-group">
128
+ <select id="openai-model-select" style="width: 100%; background: var(--input-bg); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; color: var(--text-main); font-family: inherit; font-size: 0.9rem; outline: none; transition: border-color 0.2s; cursor: pointer;">
129
+ <option value="gpt-4o" style="background: var(--bg-color)">gpt-4o</option>
130
+ <option value="gpt-4o-mini" style="background: var(--bg-color)">gpt-4o-mini</option>
131
+ <option value="o1-preview" style="background: var(--bg-color)">o1-preview</option>
132
+ <option value="o1-mini" style="background: var(--bg-color)">o1-mini</option>
133
+ <option value="custom" style="background: var(--bg-color)">Custom...</option>
134
+ </select>
135
+ </div>
136
+ </div>
137
+ <div class="setting-row" id="openai-model-custom-row" style="display: none; margin-top: 10px;">
138
+ <label for="openai-model-custom">Custom OpenAI Model</label>
139
+ <input type="text" id="openai-model-custom" placeholder="e.g. gpt-4o">
140
+ </div>
141
+
142
+ <div class="setting-row" style="margin-top: 14px;">
143
+ <label for="anthropic-model-select">Anthropic Model</label>
144
+ <div class="input-group">
145
+ <select id="anthropic-model-select" style="width: 100%; background: var(--input-bg); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; color: var(--text-main); font-family: inherit; font-size: 0.9rem; outline: none; transition: border-color 0.2s; cursor: pointer;">
146
+ <option value="claude-3-5-sonnet-latest" style="background: var(--bg-color)">claude-3-5-sonnet-latest</option>
147
+ <option value="claude-3-opus-latest" style="background: var(--bg-color)">claude-3-opus-latest</option>
148
+ <option value="claude-3-5-haiku-latest" style="background: var(--bg-color)">claude-3-5-haiku-latest</option>
149
+ <option value="custom" style="background: var(--bg-color)">Custom...</option>
150
+ </select>
151
+ </div>
152
+ </div>
153
+ <div class="setting-row" id="anthropic-model-custom-row" style="display: none; margin-top: 10px;">
154
+ <label for="anthropic-model-custom">Custom Anthropic Model</label>
155
+ <input type="text" id="anthropic-model-custom" placeholder="e.g. claude-3-5-sonnet-latest">
156
+ </div>
157
+
158
+ <div class="setting-row" style="margin-top: 14px;">
159
+ <label for="hf-model-name">Hugging Face Model</label>
160
+ <input type="text" id="hf-model-name" placeholder="e.g. meta-llama/Meta-Llama-3-8B-Instruct">
161
+ </div>
162
+
163
+ <div class="setting-row" style="margin-top: 14px;">
164
+ <label for="local-model-name">Local (LM Studio) Model</label>
165
+ <input type="text" id="local-model-name" placeholder="e.g. local-model">
166
+ </div>
167
+
168
+ <div class="setting-row" style="margin-top: 14px;">
169
+ <label for="ollama-model-input">Ollama Model</label>
170
+ <input type="text" id="ollama-model-input" placeholder="e.g. llama3:latest">
171
+ </div>
172
+ </section>
109
173
  </div>
110
174
  <div class="tab-pane" id="sect-audio">
111
175
  <!-- Audio & Voice -->
@@ -212,6 +276,20 @@
212
276
  <button class="btn-secondary" id="open-workflows-btn">Open workflows.json</button>
213
277
  <button class="btn-primary" id="reload-workflows-btn">Reload Rules</button>
214
278
  </div>
279
+ </section>
280
+ <!-- Agent Collaboration -->
281
+ <section class="setting-section">
282
+ <h2 class="section-title">🤝 Agent Collaboration</h2>
283
+ <div class="setting-row" style="display: flex; align-items: center; justify-content: space-between;">
284
+ <label for="enable-agent-collaboration" style="margin-bottom: 0;">Enable Multi-Agent Review</label>
285
+ <label class="toggle-switch">
286
+ <input type="checkbox" id="enable-agent-collaboration">
287
+ <span class="toggle-slider"></span>
288
+ </label>
289
+ </div>
290
+ <div class="setting-row" style="margin-top: 10px;">
291
+ <p class="hint">When in Code Mode, allow a secondary AI model to automatically review the code written by the primary model. (Requires multiple API keys. Costs API credits for both models.)</p>
292
+ </div>
215
293
  </section>
216
294
  </div>
217
295
  <div class="tab-pane" id="sect-theme">