@pheem49/mint 1.4.1 → 1.5.0

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 (61) hide show
  1. package/GUIDE_TH.md +113 -0
  2. package/README.md +214 -142
  3. package/assets/CLI_Screen.png +0 -0
  4. package/docs/assets/CLI_Screen.png +0 -0
  5. package/docs/guide.html +632 -0
  6. package/docs/index.html +5 -4
  7. package/main.js +66 -894
  8. package/mint-cli-logic.js +15 -8
  9. package/mint-cli.js +305 -195
  10. package/package.json +12 -4
  11. package/src/AI_Brain/Gemini_API.js +77 -20
  12. package/src/AI_Brain/agent_orchestrator.js +6 -6
  13. package/src/AI_Brain/autonomous_brain.js +10 -0
  14. package/src/AI_Brain/behavior_memory.js +26 -5
  15. package/src/AI_Brain/headless_agent.js +4 -0
  16. package/src/AI_Brain/knowledge_base.js +61 -8
  17. package/src/AI_Brain/memory_store.js +55 -7
  18. package/src/Automation_Layer/file_operations.js +14 -3
  19. package/src/CLI/chat_router.js +21 -7
  20. package/src/CLI/chat_ui.js +264 -710
  21. package/src/CLI/code_agent.js +370 -124
  22. package/src/CLI/gmail_auth.js +210 -0
  23. package/src/CLI/list_features.js +5 -1
  24. package/src/CLI/onboarding.js +307 -55
  25. package/src/CLI/updater.js +208 -0
  26. package/src/Channels/brave_search_bridge.js +35 -0
  27. package/src/Channels/discord_bridge.js +68 -0
  28. package/src/Channels/google_search_bridge.js +38 -0
  29. package/src/Channels/line_bridge.js +60 -0
  30. package/src/Channels/slack_bridge.js +53 -0
  31. package/src/Channels/telegram_bridge.js +49 -0
  32. package/src/Channels/whatsapp_bridge.js +55 -0
  33. package/src/Command_Parser/parser.js +12 -1
  34. package/src/Plugins/gmail.js +251 -0
  35. package/src/Plugins/google_calendar.js +245 -19
  36. package/src/Plugins/notion.js +256 -0
  37. package/src/System/action_executor.js +129 -0
  38. package/src/System/bridge_manager.js +76 -0
  39. package/src/System/chat_history_manager.js +23 -5
  40. package/src/System/config_manager.js +41 -7
  41. package/src/System/custom_workflows.js +31 -2
  42. package/src/System/google_tts_urls.js +51 -0
  43. package/src/System/ipc_handlers.js +238 -0
  44. package/src/System/proactive_loop.js +137 -0
  45. package/src/System/safety_manager.js +165 -0
  46. package/src/System/screen_capture.js +175 -0
  47. package/src/System/task_manager.js +15 -5
  48. package/src/System/window_manager.js +210 -0
  49. package/src/UI/renderer.js +33 -7
  50. package/src/UI/settings.html +24 -0
  51. package/src/UI/settings.js +14 -4
  52. package/src/UI/styles.css +14 -1
  53. package/tests/action_executor_safety.test.js +67 -0
  54. package/tests/gmail.test.js +135 -0
  55. package/tests/gmail_auth.test.js +129 -0
  56. package/tests/google_calendar.test.js +113 -0
  57. package/tests/google_tts_urls.test.js +24 -0
  58. package/tests/notion.test.js +121 -0
  59. package/tests/provider_routing.test.js +17 -1
  60. package/tests/safety_manager.test.js +40 -0
  61. package/tests/updater.test.js +32 -0
@@ -0,0 +1,175 @@
1
+ const { BrowserWindow, desktopCapturer, screen } = require('electron');
2
+ const path = require('path');
3
+
4
+ const TRANSLATE_REFRESH_MS = 3000;
5
+ const TRANSLATE_FAILURE_COOLDOWN_MS = 15000;
6
+
7
+ function createScreenCaptureController({ projectRoot, translateImageContent, getMainWindow }) {
8
+ let screenPickerWindow = null;
9
+ let translateIntervalHandle = null;
10
+ let isTranslateRequestInFlight = false;
11
+ let translateCooldownUntil = 0;
12
+
13
+ async function startScreenCapture() {
14
+ if (screenPickerWindow) return;
15
+
16
+ try {
17
+ const primaryDisplay = screen.getPrimaryDisplay();
18
+ const { width, height } = primaryDisplay.size;
19
+ const sources = await desktopCapturer.getSources({
20
+ types: ['screen'],
21
+ thumbnailSize: { width, height }
22
+ });
23
+ const primarySource = sources[0];
24
+
25
+ screenPickerWindow = new BrowserWindow({
26
+ width,
27
+ height,
28
+ x: primaryDisplay.bounds.x,
29
+ y: primaryDisplay.bounds.y,
30
+ fullscreen: true,
31
+ transparent: true,
32
+ frame: false,
33
+ alwaysOnTop: true,
34
+ skipTaskbar: true,
35
+ webPreferences: {
36
+ preload: path.join(projectRoot, 'preload-picker.js'),
37
+ nodeIntegration: false,
38
+ contextIsolation: true
39
+ }
40
+ });
41
+
42
+ await screenPickerWindow.loadFile(path.join(projectRoot, 'src/UI/screenPicker.html'));
43
+
44
+ if (primarySource && primarySource.thumbnail) {
45
+ screenPickerWindow.webContents.send('screenshot-data', primarySource.thumbnail.toDataURL());
46
+ }
47
+
48
+ screenPickerWindow.on('closed', () => { screenPickerWindow = null; });
49
+ } catch (err) {
50
+ console.error("Error starting screen capture:", err);
51
+ }
52
+ }
53
+
54
+ function handleSelection(base64Image) {
55
+ if (screenPickerWindow) screenPickerWindow.close();
56
+
57
+ const mainWindow = getMainWindow();
58
+ if (mainWindow) {
59
+ mainWindow.webContents.send('vision-ready', base64Image);
60
+ mainWindow.show();
61
+ }
62
+ }
63
+
64
+ function startLiveTranslate(rect) {
65
+ if (!screenPickerWindow) return;
66
+
67
+ screenPickerWindow.setIgnoreMouseEvents(true, { forward: true });
68
+ isTranslateRequestInFlight = false;
69
+ translateCooldownUntil = 0;
70
+
71
+ stopLiveTranslate(false);
72
+ captureAndTranslate(rect);
73
+ translateIntervalHandle = setInterval(() => {
74
+ captureAndTranslate(rect);
75
+ }, TRANSLATE_REFRESH_MS);
76
+ }
77
+
78
+ function stopLiveTranslate(resetMouseEvents = true) {
79
+ if (translateIntervalHandle) {
80
+ clearInterval(translateIntervalHandle);
81
+ translateIntervalHandle = null;
82
+ }
83
+ if (resetMouseEvents && screenPickerWindow && !screenPickerWindow.isDestroyed()) {
84
+ screenPickerWindow.setIgnoreMouseEvents(false);
85
+ }
86
+ isTranslateRequestInFlight = false;
87
+ translateCooldownUntil = 0;
88
+ }
89
+
90
+ function setOverlayInteractable(isInteractable) {
91
+ if (!screenPickerWindow || screenPickerWindow.isDestroyed()) return;
92
+ screenPickerWindow.setIgnoreMouseEvents(!isInteractable, { forward: true });
93
+ }
94
+
95
+ async function captureAndTranslate(rect) {
96
+ if (!screenPickerWindow || screenPickerWindow.isDestroyed()) return;
97
+ if (isTranslateRequestInFlight) return;
98
+ if (Date.now() < translateCooldownUntil) return;
99
+
100
+ try {
101
+ isTranslateRequestInFlight = true;
102
+ const primaryDisplay = screen.getPrimaryDisplay();
103
+ const sources = await desktopCapturer.getSources({
104
+ types: ['screen'],
105
+ thumbnailSize: {
106
+ width: primaryDisplay.size.width,
107
+ height: primaryDisplay.size.height
108
+ }
109
+ });
110
+
111
+ if (sources.length > 0) {
112
+ const croppedImage = sources[0].thumbnail.crop({
113
+ x: Math.round(rect.x),
114
+ y: Math.round(rect.y),
115
+ width: Math.round(rect.width),
116
+ height: Math.round(rect.height)
117
+ });
118
+
119
+ const base64Crop = croppedImage.toJPEG(70).toString('base64');
120
+ const translationResult = await translateImageContent(`data:image/jpeg;base64,${base64Crop}`);
121
+ if (translationResult.retryableFailure) {
122
+ translateCooldownUntil = Date.now() + TRANSLATE_FAILURE_COOLDOWN_MS;
123
+ console.warn(`Live translation cooldown active for ${TRANSLATE_FAILURE_COOLDOWN_MS / 1000}s after retryable API failure.`);
124
+ } else {
125
+ translateCooldownUntil = 0;
126
+ }
127
+
128
+ if (screenPickerWindow && !screenPickerWindow.isDestroyed()) {
129
+ screenPickerWindow.webContents.send('vision-translate-result', translationResult.text);
130
+ }
131
+ }
132
+ } catch (err) {
133
+ console.error("Continuous translation loop failed:", err);
134
+ } finally {
135
+ isTranslateRequestInFlight = false;
136
+ }
137
+ }
138
+
139
+ function cancel() {
140
+ stopLiveTranslate(false);
141
+ if (screenPickerWindow) screenPickerWindow.close();
142
+
143
+ const mainWindow = getMainWindow();
144
+ if (mainWindow) mainWindow.show();
145
+ }
146
+
147
+ async function captureSilentScreen() {
148
+ try {
149
+ const primaryDisplay = screen.getPrimaryDisplay();
150
+ const { width, height } = primaryDisplay.size;
151
+ const sources = await desktopCapturer.getSources({
152
+ types: ['screen'],
153
+ thumbnailSize: { width, height }
154
+ });
155
+
156
+ const primarySource = sources[0];
157
+ return primarySource && primarySource.thumbnail ? primarySource.thumbnail.toDataURL() : null;
158
+ } catch (err) {
159
+ console.error("Error silently capturing screen:", err);
160
+ return null;
161
+ }
162
+ }
163
+
164
+ return {
165
+ startScreenCapture,
166
+ handleSelection,
167
+ startLiveTranslate,
168
+ stopLiveTranslate,
169
+ setOverlayInteractable,
170
+ cancel,
171
+ captureSilentScreen
172
+ };
173
+ }
174
+
175
+ module.exports = { createScreenCaptureController };
@@ -2,13 +2,23 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
 
5
- // Standard location for Mint tasks
6
- const MINT_DIR = path.join(os.homedir(), '.mint');
7
- const TASKS_FILE = path.join(MINT_DIR, 'tasks.json');
5
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'mint');
6
+ const TASKS_FILE = path.join(CONFIG_DIR, 'tasks.json');
8
7
 
9
8
  // Ensure directory exists
10
- if (!fs.existsSync(MINT_DIR)) {
11
- fs.mkdirSync(MINT_DIR, { recursive: true });
9
+ if (!fs.existsSync(CONFIG_DIR)) {
10
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
11
+ }
12
+
13
+ // Migration Logic: Move tasks.json from ~/.mint to ~/.config/mint
14
+ if (!fs.existsSync(TASKS_FILE)) {
15
+ const legacyPath = path.join(os.homedir(), '.mint', 'tasks.json');
16
+ if (fs.existsSync(legacyPath)) {
17
+ try {
18
+ fs.copyFileSync(legacyPath, TASKS_FILE);
19
+ console.log('[TaskManager] Migrated tasks from ~/.mint');
20
+ } catch (e) { console.error('[TaskManager] Migration failed:', e); }
21
+ }
12
22
  }
13
23
 
14
24
  /**
@@ -0,0 +1,210 @@
1
+ const { app, BrowserWindow, Tray, Menu, nativeImage, screen } = require('electron');
2
+ const path = require('path');
3
+
4
+ function createWindowManager(projectRoot) {
5
+ let mainWindow = null;
6
+ let settingsWindow = null;
7
+ let spotlightWindow = null;
8
+ let widgetWindow = null;
9
+ let tray = null;
10
+
11
+ function createMainWindow() {
12
+ mainWindow = new BrowserWindow({
13
+ width: 600,
14
+ height: 800,
15
+ icon: path.join(projectRoot, 'assets', 'icon.png'),
16
+ webPreferences: {
17
+ preload: path.join(projectRoot, 'preload.js'),
18
+ nodeIntegration: false,
19
+ contextIsolation: true,
20
+ },
21
+ frame: false,
22
+ transparent: true,
23
+ resizable: true,
24
+ show: false
25
+ });
26
+
27
+ mainWindow.loadFile(path.join(projectRoot, 'src/UI/index.html'));
28
+ mainWindow.on('ready-to-show', () => mainWindow.show());
29
+ mainWindow.on('close', (event) => {
30
+ if (!app.isQuiting) {
31
+ event.preventDefault();
32
+ mainWindow.hide();
33
+ }
34
+ return false;
35
+ });
36
+ mainWindow.on('focus', () => {
37
+ // clearFloatingUnread(); // Disabled
38
+ });
39
+ mainWindow.on('closed', () => {
40
+ mainWindow = null;
41
+ });
42
+
43
+ return mainWindow;
44
+ }
45
+
46
+ function createTray() {
47
+ const iconPath = path.join(projectRoot, 'assets', 'icon.png');
48
+ let icon = nativeImage.createFromPath(iconPath);
49
+ icon = icon.resize({ width: 16, height: 16 });
50
+
51
+ tray = new Tray(icon);
52
+ tray.setToolTip('Mint AI Assistant');
53
+ tray.setContextMenu(Menu.buildFromTemplate([
54
+ { label: 'Show App', click: () => { if (mainWindow) mainWindow.show(); } },
55
+ { label: 'Settings', click: () => { createSettingsWindow(); } },
56
+ { type: 'separator' },
57
+ {
58
+ label: 'Quit',
59
+ click: () => {
60
+ app.isQuiting = true;
61
+ app.quit();
62
+ }
63
+ }
64
+ ]));
65
+
66
+ tray.on('click', toggleMainWindow);
67
+ return tray;
68
+ }
69
+
70
+ function createSettingsWindow() {
71
+ if (settingsWindow) {
72
+ settingsWindow.focus();
73
+ return settingsWindow;
74
+ }
75
+
76
+ settingsWindow = new BrowserWindow({
77
+ width: 720,
78
+ height: 620,
79
+ minWidth: 640,
80
+ minHeight: 560,
81
+ icon: path.join(projectRoot, 'assets', 'icon.png'),
82
+ webPreferences: {
83
+ preload: path.join(projectRoot, 'preload-settings.js'),
84
+ nodeIntegration: false,
85
+ contextIsolation: true,
86
+ },
87
+ frame: false,
88
+ transparent: true,
89
+ resizable: true,
90
+ parent: mainWindow,
91
+ });
92
+ settingsWindow.loadFile(path.join(projectRoot, 'src/UI/settings.html'));
93
+ settingsWindow.on('closed', () => { settingsWindow = null; });
94
+ return settingsWindow;
95
+ }
96
+
97
+ function createSpotlightWindow() {
98
+ if (spotlightWindow) {
99
+ spotlightWindow.show();
100
+ return spotlightWindow;
101
+ }
102
+
103
+ const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
104
+ const windowWidth = 600;
105
+ const windowHeight = 80;
106
+
107
+ spotlightWindow = new BrowserWindow({
108
+ width: windowWidth,
109
+ height: windowHeight,
110
+ x: Math.floor((screenWidth - windowWidth) / 2),
111
+ y: Math.floor(screenHeight * 0.25),
112
+ frame: false,
113
+ transparent: true,
114
+ alwaysOnTop: true,
115
+ skipTaskbar: true,
116
+ show: false,
117
+ webPreferences: {
118
+ preload: path.join(projectRoot, 'src/UI/preload-spotlight.js'),
119
+ nodeIntegration: false,
120
+ contextIsolation: true,
121
+ }
122
+ });
123
+
124
+ spotlightWindow.loadFile(path.join(projectRoot, 'src/UI/spotlight.html'));
125
+ spotlightWindow.on('blur', () => spotlightWindow.hide());
126
+ spotlightWindow.on('closed', () => { spotlightWindow = null; });
127
+ return spotlightWindow;
128
+ }
129
+
130
+ function createWidgetWindow() {
131
+ if (widgetWindow) return widgetWindow;
132
+
133
+ widgetWindow = new BrowserWindow({
134
+ width: 150,
135
+ height: 150,
136
+ frame: false,
137
+ transparent: true,
138
+ resizable: false,
139
+ alwaysOnTop: true,
140
+ skipTaskbar: true,
141
+ show: true,
142
+ webPreferences: {
143
+ preload: path.join(projectRoot, 'src/UI/preload-widget.js'),
144
+ nodeIntegration: false,
145
+ contextIsolation: true
146
+ }
147
+ });
148
+
149
+ widgetWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
150
+ widgetWindow.setAlwaysOnTop(true, 'floating');
151
+
152
+ try {
153
+ const primaryDisplay = screen.getPrimaryDisplay();
154
+ const { width, x, y } = primaryDisplay.workArea;
155
+ widgetWindow.setPosition(x + width - 150 - 40, y + 40);
156
+ } catch (_) {}
157
+
158
+ widgetWindow.loadFile(path.join(projectRoot, 'src/UI/widget.html'));
159
+ widgetWindow.on('closed', () => { widgetWindow = null; });
160
+ return widgetWindow;
161
+ }
162
+
163
+ function toggleMainWindow() {
164
+ if (!mainWindow) return;
165
+ if (mainWindow.isVisible()) {
166
+ mainWindow.hide();
167
+ } else {
168
+ mainWindow.show();
169
+ }
170
+ }
171
+
172
+ function toggleSpotlightWindow() {
173
+ if (spotlightWindow && spotlightWindow.isVisible()) {
174
+ spotlightWindow.hide();
175
+ return;
176
+ }
177
+ createSpotlightWindow().show();
178
+ }
179
+
180
+ function closeWidgetWindow() {
181
+ if (widgetWindow && !widgetWindow.isDestroyed()) {
182
+ widgetWindow.close();
183
+ widgetWindow = null;
184
+ }
185
+ }
186
+
187
+ function ensureWidgetWindow() {
188
+ if (!widgetWindow || widgetWindow.isDestroyed()) {
189
+ createWidgetWindow();
190
+ }
191
+ }
192
+
193
+ return {
194
+ createMainWindow,
195
+ createTray,
196
+ createSettingsWindow,
197
+ createSpotlightWindow,
198
+ createWidgetWindow,
199
+ toggleMainWindow,
200
+ toggleSpotlightWindow,
201
+ closeWidgetWindow,
202
+ ensureWidgetWindow,
203
+ getMainWindow: () => mainWindow,
204
+ getSettingsWindow: () => settingsWindow,
205
+ getSpotlightWindow: () => spotlightWindow,
206
+ getWidgetWindow: () => widgetWindow
207
+ };
208
+ }
209
+
210
+ module.exports = { createWindowManager };
@@ -558,7 +558,15 @@ clearBtn.addEventListener('click', async () => {
558
558
  appendMessage('Chat history cleared. Starting fresh! 🌿', 'ai', null, new Date().toISOString());
559
559
  });
560
560
 
561
- function appendMessage(text, sender, base64Image = null, timestamp = null) {
561
+ function formatProviderInfo(providerInfo) {
562
+ if (!providerInfo || typeof providerInfo !== 'object') return '';
563
+ const provider = String(providerInfo.provider || '').trim();
564
+ const model = String(providerInfo.model || '').trim();
565
+ if (!provider && !model) return '';
566
+ return model ? `${provider || 'AI'} • ${model}` : provider;
567
+ }
568
+
569
+ function appendMessage(text, sender, base64Image = null, timestamp = null, options = {}) {
562
570
  const messageDiv = document.createElement('div');
563
571
  messageDiv.classList.add('message', `${sender}-message`);
564
572
 
@@ -586,11 +594,23 @@ function appendMessage(text, sender, base64Image = null, timestamp = null) {
586
594
 
587
595
  bubbleWrapper.appendChild(bubble);
588
596
 
589
- // Add Timestamp
590
- if (timestamp) {
597
+ const providerLabel = sender === 'ai' ? formatProviderInfo(options.providerInfo) : '';
598
+
599
+ // Add metadata
600
+ if (timestamp || providerLabel) {
591
601
  const timeDiv = document.createElement('div');
592
602
  timeDiv.classList.add('message-time');
593
- timeDiv.textContent = formatTime(timestamp);
603
+ if (providerLabel) {
604
+ const providerSpan = document.createElement('span');
605
+ providerSpan.classList.add('provider-badge');
606
+ providerSpan.textContent = providerLabel;
607
+ timeDiv.appendChild(providerSpan);
608
+ }
609
+ if (timestamp) {
610
+ const timeSpan = document.createElement('span');
611
+ timeSpan.textContent = formatTime(timestamp);
612
+ timeDiv.appendChild(timeSpan);
613
+ }
594
614
  bubbleWrapper.appendChild(timeDiv);
595
615
  }
596
616
 
@@ -638,6 +658,7 @@ function estimateMessageDelay(text) {
638
658
  async function appendAiMessages(text, options = {}) {
639
659
  const allowDelay = options.allowDelay !== false;
640
660
  const timestamp = options.timestamp || new Date().toISOString();
661
+ const providerInfo = options.providerInfo || null;
641
662
  const parts = splitAiMessages(text);
642
663
  let lastDiv = null;
643
664
 
@@ -649,7 +670,8 @@ async function appendAiMessages(text, options = {}) {
649
670
  }
650
671
  // Only show timestamp for the last bubble in a group if multiple
651
672
  const partTimestamp = (index === parts.length - 1) ? timestamp : null;
652
- lastDiv = appendMessage(parts[index], 'ai', null, partTimestamp);
673
+ const partProviderInfo = (index === parts.length - 1) ? providerInfo : null;
674
+ lastDiv = appendMessage(parts[index], 'ai', null, partTimestamp, { providerInfo: partProviderInfo });
653
675
  }
654
676
 
655
677
  return lastDiv;
@@ -750,7 +772,7 @@ async function loadChatHistory() {
750
772
  if (!item || typeof item.text !== 'string' || !item.text.trim()) continue;
751
773
  const sender = item.sender === 'user' ? 'user' : 'ai';
752
774
  if (sender === 'ai') {
753
- await appendAiMessages(item.text, { allowDelay: false, timestamp: item.timestamp });
775
+ await appendAiMessages(item.text, { allowDelay: false, timestamp: item.timestamp, providerInfo: item.providerInfo });
754
776
  } else {
755
777
  appendMessage(item.text, sender, null, item.timestamp);
756
778
  }
@@ -829,7 +851,11 @@ async function sendTextMessage(text, options = {}) {
829
851
  }
830
852
 
831
853
  // Show AI response
832
- const msgDiv = await appendAiMessages(response.response, { allowDelay: true });
854
+ const msgDiv = await appendAiMessages(response.response, {
855
+ allowDelay: true,
856
+ timestamp: response.timestamp,
857
+ providerInfo: response.providerInfo
858
+ });
833
859
 
834
860
  // Speak AI response
835
861
  await speakText(normalizeAiText(response.response), { onEnd: resumeSpeechIfNeeded });
@@ -417,6 +417,30 @@
417
417
  </div>
418
418
  </div>
419
419
 
420
+ <!-- Gmail -->
421
+ <div class="plugin-card">
422
+ <div class="plugin-icon">✉️</div>
423
+ <div class="plugin-info">
424
+ <div class="plugin-name">Gmail</div>
425
+ <div class="plugin-desc">Read email and create drafts safely</div>
426
+ </div>
427
+ <div class="plugin-actions">
428
+ <button class="btn-connect" id="btn-plugin-gmail" data-plugin="gmail">Connect</button>
429
+ </div>
430
+ </div>
431
+
432
+ <!-- Notion -->
433
+ <div class="plugin-card">
434
+ <div class="plugin-icon">📝</div>
435
+ <div class="plugin-info">
436
+ <div class="plugin-name">Notion</div>
437
+ <div class="plugin-desc">Create notes, pages, and read databases</div>
438
+ </div>
439
+ <div class="plugin-actions">
440
+ <button class="btn-connect" id="btn-plugin-notion" data-plugin="notion">Connect</button>
441
+ </div>
442
+ </div>
443
+
420
444
  <!-- Discord -->
421
445
  <div class="plugin-card">
422
446
  <div class="plugin-icon">💬</div>
@@ -23,9 +23,17 @@ const DEFAULT_CONFIG = {
23
23
  ttsPitch: 1.0,
24
24
  pluginSpotifyEnabled: true,
25
25
  pluginCalendarEnabled: false,
26
+ pluginGmailEnabled: false,
27
+ pluginNotionEnabled: false,
26
28
  pluginDiscordEnabled: false,
27
29
  showDesktopWidget: true,
28
- mcpServers: {}
30
+ mcpServers: {},
31
+ openaiModel: 'gpt-4o',
32
+ anthropicModel: 'claude-3-5-sonnet-latest',
33
+ hfModel: 'meta-llama/Meta-Llama-3-8B-Instruct',
34
+ localApiBaseUrl: '',
35
+ localModelName: 'local-model',
36
+ ollamaHost: ''
29
37
  };
30
38
 
31
39
  let currentConfig = { ...DEFAULT_CONFIG };
@@ -68,7 +76,7 @@ function applyConfig(config) {
68
76
  if (hfInput) hfInput.value = config.hfApiKey || '';
69
77
 
70
78
  const ollamaHostInput = document.getElementById('ollama-host-input');
71
- if (ollamaHostInput) ollamaHostInput.value = config.ollamaHost || 'http://localhost:11434';
79
+ if (ollamaHostInput) ollamaHostInput.value = config.ollamaHost || '';
72
80
 
73
81
  // Apply Gemini model
74
82
  applyModelSelection(config.geminiModel);
@@ -95,7 +103,7 @@ function applyConfig(config) {
95
103
 
96
104
  const localApiBaseUrlInput = document.getElementById('local-api-base-url');
97
105
  if (localApiBaseUrlInput) {
98
- localApiBaseUrlInput.value = config.localApiBaseUrl || 'http://localhost:1234/v1';
106
+ localApiBaseUrlInput.value = config.localApiBaseUrl || '';
99
107
  }
100
108
 
101
109
  const localModelNameInput = document.getElementById('local-model-name');
@@ -142,6 +150,8 @@ function applyConfig(config) {
142
150
  // Plugins logic
143
151
  updatePluginButton('spotify', config.pluginSpotifyEnabled);
144
152
  updatePluginButton('calendar', config.pluginCalendarEnabled);
153
+ updatePluginButton('gmail', config.pluginGmailEnabled);
154
+ updatePluginButton('notion', config.pluginNotionEnabled);
145
155
  updatePluginButton('discord', config.pluginDiscordEnabled);
146
156
 
147
157
  // Apply Automation Browser
@@ -691,7 +701,7 @@ function updatePluginButton(pluginName, isEnabled) {
691
701
  }
692
702
 
693
703
  // Bind plugin buttons
694
- ['spotify', 'calendar', 'discord'].forEach(plugin => {
704
+ ['spotify', 'calendar', 'gmail', 'notion', 'discord'].forEach(plugin => {
695
705
  const btn = document.getElementById(`btn-plugin-${plugin}`);
696
706
  if (btn) {
697
707
  btn.addEventListener('click', () => {
package/src/UI/styles.css CHANGED
@@ -251,6 +251,19 @@ h1 {
251
251
  padding: 0 6px;
252
252
  opacity: 0.7;
253
253
  font-weight: 400;
254
+ display: flex;
255
+ align-items: center;
256
+ gap: 8px;
257
+ flex-wrap: wrap;
258
+ }
259
+
260
+ .provider-badge {
261
+ max-width: min(320px, 70vw);
262
+ overflow: hidden;
263
+ text-overflow: ellipsis;
264
+ white-space: nowrap;
265
+ color: var(--accent);
266
+ font-weight: 600;
254
267
  }
255
268
 
256
269
  @keyframes messagePopIn {
@@ -648,4 +661,4 @@ input:checked + .slider:before {
648
661
 
649
662
  .suggestion-chip:active {
650
663
  transform: scale(0.97);
651
- }
664
+ }
@@ -0,0 +1,67 @@
1
+ jest.mock('electron', () => ({
2
+ clipboard: {
3
+ writeText: jest.fn()
4
+ }
5
+ }));
6
+
7
+ jest.mock('../src/Automation_Layer/file_operations', () => ({
8
+ createFolder: jest.fn(() => ({ success: true })),
9
+ openFile: jest.fn(async () => true),
10
+ deleteFile: jest.fn(async () => ({ success: true })),
11
+ findPath: jest.fn(() => ({ success: false, message: 'not found', matches: [] }))
12
+ }));
13
+
14
+ jest.mock('../src/Automation_Layer/open_app', () => ({
15
+ openApp: jest.fn()
16
+ }));
17
+
18
+ jest.mock('../src/Automation_Layer/open_website', () => ({
19
+ openWebsite: jest.fn(),
20
+ openSearch: jest.fn()
21
+ }));
22
+
23
+ jest.mock('../src/Automation_Layer/browser_automation', () => ({
24
+ performWebAutomation: jest.fn(async () => 'done')
25
+ }));
26
+
27
+ jest.mock('../src/AI_Brain/knowledge_base', () => ({
28
+ indexFile: jest.fn(async () => 'indexed'),
29
+ indexFolder: jest.fn(async () => 'indexed')
30
+ }));
31
+
32
+ jest.mock('../src/Plugins/plugin_manager', () => ({
33
+ executePlugin: jest.fn()
34
+ }));
35
+
36
+ jest.mock('../src/Plugins/mcp_manager', () => ({
37
+ callTool: jest.fn()
38
+ }));
39
+
40
+ jest.mock('../src/System/granular_automation', () => ({
41
+ mouseMove: jest.fn(),
42
+ mouseClick: jest.fn(),
43
+ typeText: jest.fn(),
44
+ keyTap: jest.fn()
45
+ }));
46
+
47
+ jest.mock('../src/System/system_automation', () => ({
48
+ shutdown: jest.fn(),
49
+ restart: jest.fn(),
50
+ sleep: jest.fn(),
51
+ setVolume: jest.fn(),
52
+ mute: jest.fn(),
53
+ setBrightness: jest.fn(),
54
+ minimizeAll: jest.fn()
55
+ }));
56
+
57
+ describe('action_executor safety integration', () => {
58
+ test('blocks dangerous delete actions unless explicitly allowed', async () => {
59
+ const { executeAction } = require('../src/System/action_executor');
60
+ await expect(executeAction({ type: 'delete_file', target: 'notes.txt' })).rejects.toThrow(/Dangerous action/);
61
+ });
62
+
63
+ test('allows dangerous actions with explicit permission flag', async () => {
64
+ const { executeAction } = require('../src/System/action_executor');
65
+ await expect(executeAction({ type: 'delete_file', target: 'notes.txt' }, { allowDangerous: true })).resolves.toBeUndefined();
66
+ });
67
+ });