@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.
- package/GUIDE_TH.md +113 -0
- package/README.md +239 -76
- 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 +13 -1
- package/mint-cli.js +100 -9
- package/package.json +12 -4
- package/src/AI_Brain/Gemini_API.js +77 -20
- 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 +1 -1
- package/src/CLI/chat_router.js +3 -2
- package/src/CLI/chat_ui.js +263 -838
- package/src/CLI/code_agent.js +144 -42
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/list_features.js +2 -0
- 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,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
|
-
|
|
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>
|