@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.
- package/GUIDE_TH.md +113 -0
- package/README.md +214 -142
- package/assets/CLI_Screen.png +0 -0
- package/docs/assets/CLI_Screen.png +0 -0
- package/docs/guide.html +632 -0
- package/docs/index.html +5 -4
- package/main.js +66 -894
- package/mint-cli-logic.js +15 -8
- package/mint-cli.js +305 -195
- package/package.json +12 -4
- package/src/AI_Brain/Gemini_API.js +77 -20
- package/src/AI_Brain/agent_orchestrator.js +6 -6
- package/src/AI_Brain/autonomous_brain.js +10 -0
- package/src/AI_Brain/behavior_memory.js +26 -5
- package/src/AI_Brain/headless_agent.js +4 -0
- package/src/AI_Brain/knowledge_base.js +61 -8
- package/src/AI_Brain/memory_store.js +55 -7
- package/src/Automation_Layer/file_operations.js +14 -3
- package/src/CLI/chat_router.js +21 -7
- package/src/CLI/chat_ui.js +264 -710
- package/src/CLI/code_agent.js +370 -124
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/list_features.js +5 -1
- package/src/CLI/onboarding.js +307 -55
- package/src/CLI/updater.js +208 -0
- package/src/Channels/brave_search_bridge.js +35 -0
- package/src/Channels/discord_bridge.js +68 -0
- package/src/Channels/google_search_bridge.js +38 -0
- package/src/Channels/line_bridge.js +60 -0
- package/src/Channels/slack_bridge.js +53 -0
- package/src/Channels/telegram_bridge.js +49 -0
- package/src/Channels/whatsapp_bridge.js +55 -0
- package/src/Command_Parser/parser.js +12 -1
- package/src/Plugins/gmail.js +251 -0
- package/src/Plugins/google_calendar.js +245 -19
- package/src/Plugins/notion.js +256 -0
- package/src/System/action_executor.js +129 -0
- package/src/System/bridge_manager.js +76 -0
- package/src/System/chat_history_manager.js +23 -5
- package/src/System/config_manager.js +41 -7
- package/src/System/custom_workflows.js +31 -2
- package/src/System/google_tts_urls.js +51 -0
- package/src/System/ipc_handlers.js +238 -0
- package/src/System/proactive_loop.js +137 -0
- package/src/System/safety_manager.js +165 -0
- package/src/System/screen_capture.js +175 -0
- package/src/System/task_manager.js +15 -5
- package/src/System/window_manager.js +210 -0
- package/src/UI/renderer.js +33 -7
- package/src/UI/settings.html +24 -0
- package/src/UI/settings.js +14 -4
- package/src/UI/styles.css +14 -1
- package/tests/action_executor_safety.test.js +67 -0
- package/tests/gmail.test.js +135 -0
- package/tests/gmail_auth.test.js +129 -0
- package/tests/google_calendar.test.js +113 -0
- package/tests/google_tts_urls.test.js +24 -0
- package/tests/notion.test.js +121 -0
- package/tests/provider_routing.test.js +17 -1
- package/tests/safety_manager.test.js +40 -0
- 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
|
-
|
|
6
|
-
const
|
|
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(
|
|
11
|
-
fs.mkdirSync(
|
|
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 };
|
package/src/UI/renderer.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
590
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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 });
|
package/src/UI/settings.html
CHANGED
|
@@ -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>
|
package/src/UI/settings.js
CHANGED
|
@@ -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 || '
|
|
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 || '
|
|
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
|
+
});
|