@pheem49/mint 1.4.2 → 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 (60) hide show
  1. package/GUIDE_TH.md +113 -0
  2. package/README.md +239 -76
  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 +13 -1
  9. package/mint-cli.js +100 -9
  10. package/package.json +12 -4
  11. package/src/AI_Brain/Gemini_API.js +77 -20
  12. package/src/AI_Brain/autonomous_brain.js +10 -0
  13. package/src/AI_Brain/behavior_memory.js +26 -5
  14. package/src/AI_Brain/headless_agent.js +4 -0
  15. package/src/AI_Brain/knowledge_base.js +61 -8
  16. package/src/AI_Brain/memory_store.js +55 -7
  17. package/src/Automation_Layer/file_operations.js +1 -1
  18. package/src/CLI/chat_router.js +3 -2
  19. package/src/CLI/chat_ui.js +263 -838
  20. package/src/CLI/code_agent.js +144 -42
  21. package/src/CLI/gmail_auth.js +210 -0
  22. package/src/CLI/list_features.js +2 -0
  23. package/src/CLI/onboarding.js +307 -55
  24. package/src/CLI/updater.js +208 -0
  25. package/src/Channels/brave_search_bridge.js +35 -0
  26. package/src/Channels/discord_bridge.js +68 -0
  27. package/src/Channels/google_search_bridge.js +38 -0
  28. package/src/Channels/line_bridge.js +60 -0
  29. package/src/Channels/slack_bridge.js +53 -0
  30. package/src/Channels/telegram_bridge.js +49 -0
  31. package/src/Channels/whatsapp_bridge.js +55 -0
  32. package/src/Command_Parser/parser.js +12 -1
  33. package/src/Plugins/gmail.js +251 -0
  34. package/src/Plugins/google_calendar.js +245 -19
  35. package/src/Plugins/notion.js +256 -0
  36. package/src/System/action_executor.js +129 -0
  37. package/src/System/bridge_manager.js +76 -0
  38. package/src/System/chat_history_manager.js +23 -5
  39. package/src/System/config_manager.js +41 -7
  40. package/src/System/custom_workflows.js +31 -2
  41. package/src/System/google_tts_urls.js +51 -0
  42. package/src/System/ipc_handlers.js +238 -0
  43. package/src/System/proactive_loop.js +137 -0
  44. package/src/System/safety_manager.js +165 -0
  45. package/src/System/screen_capture.js +175 -0
  46. package/src/System/task_manager.js +15 -5
  47. package/src/System/window_manager.js +210 -0
  48. package/src/UI/renderer.js +33 -7
  49. package/src/UI/settings.html +24 -0
  50. package/src/UI/settings.js +14 -4
  51. package/src/UI/styles.css +14 -1
  52. package/tests/action_executor_safety.test.js +67 -0
  53. package/tests/gmail.test.js +135 -0
  54. package/tests/gmail_auth.test.js +129 -0
  55. package/tests/google_calendar.test.js +113 -0
  56. package/tests/google_tts_urls.test.js +24 -0
  57. package/tests/notion.test.js +121 -0
  58. package/tests/provider_routing.test.js +17 -1
  59. package/tests/safety_manager.test.js +40 -0
  60. package/tests/updater.test.js +32 -0
@@ -0,0 +1,165 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const TIERS = Object.freeze({
6
+ SAFE: 'safe',
7
+ APPROVAL: 'approval',
8
+ DANGEROUS: 'dangerous',
9
+ BLOCKED: 'blocked'
10
+ });
11
+
12
+ const BLOCKED_COMMAND_PATTERNS = [
13
+ { pattern: /\brm\s+(-[^\s]*r[^\s]*f|-rf|-fr)\b/, reason: 'recursive force delete' },
14
+ { pattern: /\bgit\s+reset\s+--hard\b/, reason: 'destructive git reset' },
15
+ { pattern: /\bgit\s+checkout\s+--\b/, reason: 'destructive git checkout path restore' },
16
+ { pattern: /\bgit\s+clean\b.*\s-[^\s]*f/, reason: 'destructive git clean' },
17
+ { pattern: /\bmkfs(?:\.\w+)?\b/, reason: 'filesystem formatting' },
18
+ { pattern: /\bdd\s+.*\bof=\/dev\//, reason: 'raw disk write' },
19
+ { pattern: />\s*\/dev\/(?:sd|nvme|hd|mapper)/, reason: 'write redirection to block device' },
20
+ { pattern: /\b(shutdown|reboot|poweroff|halt)\b/, reason: 'system power command' },
21
+ { pattern: /\bsudo\b/, reason: 'privilege escalation' },
22
+ { pattern: /\bchmod\s+-R\s+777\b/, reason: 'unsafe recursive permissions' },
23
+ { pattern: /\bchown\s+-R\b/, reason: 'unsafe recursive ownership change' },
24
+ { pattern: /\bcurl\b.*\|\s*(sh|bash|zsh)\b/, reason: 'remote script piping' },
25
+ { pattern: /\bwget\b.*\|\s*(sh|bash|zsh)\b/, reason: 'remote script piping' }
26
+ ];
27
+
28
+ const DANGEROUS_ACTIONS = new Set([
29
+ 'delete_file',
30
+ 'system_automation'
31
+ ]);
32
+
33
+ const SAFE_ACTIONS = new Set([
34
+ 'open_url',
35
+ 'search',
36
+ 'open_app',
37
+ 'open_file',
38
+ 'open_folder',
39
+ 'find_path',
40
+ 'clipboard_write',
41
+ 'learn_file',
42
+ 'learn_folder',
43
+ 'mcp_tool',
44
+ 'mouse_move',
45
+ 'mouse_click',
46
+ 'type_text',
47
+ 'key_tap',
48
+ 'plugin',
49
+ 'web_automation',
50
+ 'create_folder'
51
+ ]);
52
+
53
+ const DANGEROUS_SYSTEM_COMMANDS = new Set(['shutdown', 'restart', 'reboot', 'poweroff', 'sleep']);
54
+
55
+ function normalizeCommand(command) {
56
+ return String(command || '').replace(/\s+/g, ' ').trim();
57
+ }
58
+
59
+ function classifyShellCommand(command) {
60
+ const normalized = normalizeCommand(command);
61
+ if (!normalized) {
62
+ return { tier: TIERS.BLOCKED, reason: 'empty shell command' };
63
+ }
64
+
65
+ for (const rule of BLOCKED_COMMAND_PATTERNS) {
66
+ if (rule.pattern.test(normalized)) {
67
+ return { tier: TIERS.BLOCKED, reason: rule.reason };
68
+ }
69
+ }
70
+
71
+ return { tier: TIERS.APPROVAL, reason: 'shell command requires approval' };
72
+ }
73
+
74
+ function assertShellCommandAllowed(command) {
75
+ const result = classifyShellCommand(command);
76
+ if (result.tier === TIERS.BLOCKED) {
77
+ throw new Error(`Blocked unsafe command (${result.reason}): ${command}`);
78
+ }
79
+ return result;
80
+ }
81
+
82
+ function classifyAction(action = {}) {
83
+ const type = action.type || 'none';
84
+ if (type === 'none') return { tier: TIERS.SAFE, reason: 'no-op action' };
85
+
86
+ if (type === 'system_automation') {
87
+ const command = String(action.target || '').split(':')[0];
88
+ if (DANGEROUS_SYSTEM_COMMANDS.has(command)) {
89
+ return { tier: TIERS.DANGEROUS, reason: `system automation command '${command}'` };
90
+ }
91
+ return { tier: TIERS.APPROVAL, reason: 'system automation requires approval' };
92
+ }
93
+
94
+ if (DANGEROUS_ACTIONS.has(type)) {
95
+ return { tier: TIERS.DANGEROUS, reason: `${type} can affect user data or system state` };
96
+ }
97
+
98
+ if (SAFE_ACTIONS.has(type)) {
99
+ return { tier: TIERS.SAFE, reason: 'allowed action' };
100
+ }
101
+
102
+ return { tier: TIERS.APPROVAL, reason: 'unknown action requires approval' };
103
+ }
104
+
105
+ function assertActionAllowed(action, options = {}) {
106
+ const classification = classifyAction(action);
107
+ const allowDangerous = options.allowDangerous === true;
108
+
109
+ if (classification.tier === TIERS.BLOCKED) {
110
+ throw new Error(`Blocked action (${classification.reason}): ${action.type}`);
111
+ }
112
+
113
+ if (classification.tier === TIERS.DANGEROUS && !allowDangerous) {
114
+ throw new Error(`Dangerous action requires explicit permission (${classification.reason}): ${action.type}`);
115
+ }
116
+
117
+ return classification;
118
+ }
119
+
120
+ function resolveWithinRoot(root, targetPath) {
121
+ if (!root) throw new Error('Root path is required.');
122
+ if (!targetPath) throw new Error('Target path is required.');
123
+
124
+ const resolvedRoot = path.resolve(root);
125
+ const expandedTarget = String(targetPath).startsWith('~/')
126
+ ? path.join(os.homedir(), String(targetPath).slice(2))
127
+ : targetPath;
128
+ const resolvedTarget = path.resolve(resolvedRoot, expandedTarget);
129
+ const relative = path.relative(resolvedRoot, resolvedTarget);
130
+
131
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
132
+ throw new Error(`Path is outside allowed root: ${targetPath}`);
133
+ }
134
+
135
+ return resolvedTarget;
136
+ }
137
+
138
+ function appendActionLog(entry, options = {}) {
139
+ const logPath = options.logPath || path.join(os.homedir(), '.config', 'mint', 'action-log.jsonl');
140
+ const payload = {
141
+ time: new Date().toISOString(),
142
+ ...entry
143
+ };
144
+
145
+ try {
146
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
147
+ fs.appendFileSync(logPath, `${JSON.stringify(payload)}\n`, 'utf8');
148
+ } catch (error) {
149
+ if (process.env.MINT_DEBUG === '1') {
150
+ console.error('[Safety] Failed to append action log:', error.message);
151
+ }
152
+ }
153
+
154
+ return payload;
155
+ }
156
+
157
+ module.exports = {
158
+ TIERS,
159
+ classifyShellCommand,
160
+ assertShellCommandAllowed,
161
+ classifyAction,
162
+ assertActionAllowed,
163
+ resolveWithinRoot,
164
+ appendActionLog
165
+ };
@@ -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>