@pheem49/mint 1.5.3 → 1.5.5
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 +16 -4
- package/README.md +17 -1
- package/install.ps1 +64 -0
- package/install.sh +54 -0
- package/package.json +8 -3
- package/preload.js +2 -0
- package/src/AI_Brain/Gemini_API.js +71 -24
- package/src/AI_Brain/agent_orchestrator.js +1 -1
- package/src/AI_Brain/provider_adapter.js +8 -1
- package/src/Automation_Layer/file_operations.js +17 -5
- package/src/CLI/chat_ui.js +17 -4
- package/src/CLI/cli_colors.js +1 -1
- package/src/CLI/code_agent.js +35 -1
- package/src/CLI/interactive_chat.js +22 -5
- package/src/System/config_manager.js +1 -0
- package/src/System/ipc_handlers.js +85 -1
- package/src/System/smart_context.js +227 -0
- package/src/UI/preload-spotlight.js +1 -0
- package/src/UI/renderer.js +380 -21
- package/src/UI/settings.js +1 -0
- package/src/UI/spotlight.js +13 -9
- package/src/UI/styles.css +303 -4
|
@@ -119,7 +119,7 @@ function buildExitSummary(stats) {
|
|
|
119
119
|
const activeMs = stats.agentActiveMs + (stats.activeStartedAt ? Date.now() - stats.activeStartedAt : 0);
|
|
120
120
|
const total = stats.toolCalls.total;
|
|
121
121
|
return {
|
|
122
|
-
message: '
|
|
122
|
+
message: 'Goodbye! See you again.',
|
|
123
123
|
sessionId: stats.sessionId,
|
|
124
124
|
toolCalls: {
|
|
125
125
|
...stats.toolCalls,
|
|
@@ -327,6 +327,7 @@ async function runAgentTask(text, { appendMessage, streamMessage, setThinking, r
|
|
|
327
327
|
askUser,
|
|
328
328
|
provider: preferredProvider,
|
|
329
329
|
history: contextualHistory,
|
|
330
|
+
signal: sharedState.abortController?.signal,
|
|
330
331
|
onProgress: (info) => {
|
|
331
332
|
if (info && info.phase === 'tool_call') {
|
|
332
333
|
sharedState.stats.toolCalls.total += 1;
|
|
@@ -381,6 +382,7 @@ async function startInteractiveChat(initialMessage = null, options = {}) {
|
|
|
381
382
|
lastResponseText: '',
|
|
382
383
|
recentImageContextText: '',
|
|
383
384
|
isBusy: false,
|
|
385
|
+
abortController: null,
|
|
384
386
|
stats: createSessionStats()
|
|
385
387
|
};
|
|
386
388
|
|
|
@@ -396,12 +398,19 @@ async function startInteractiveChat(initialMessage = null, options = {}) {
|
|
|
396
398
|
}
|
|
397
399
|
},
|
|
398
400
|
|
|
401
|
+
onCancel: () => {
|
|
402
|
+
if (sharedState.abortController) {
|
|
403
|
+
sharedState.abortController.abort();
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
|
|
399
407
|
onSubmit: async (text, submitOptions = {}) => {
|
|
400
408
|
if (sharedState.isBusy) {
|
|
401
409
|
ui.appendMessage('system', 'Mint is still working on the previous request. Please wait for it to finish before sending another command.');
|
|
402
410
|
return;
|
|
403
411
|
}
|
|
404
412
|
sharedState.isBusy = true;
|
|
413
|
+
sharedState.abortController = new AbortController();
|
|
405
414
|
|
|
406
415
|
const {
|
|
407
416
|
appendMessage, streamMessage, setThinking, updateStatusModel,
|
|
@@ -574,6 +583,7 @@ async function startInteractiveChat(initialMessage = null, options = {}) {
|
|
|
574
583
|
}, sharedState);
|
|
575
584
|
} finally {
|
|
576
585
|
sharedState.isBusy = false;
|
|
586
|
+
sharedState.abortController = null;
|
|
577
587
|
}
|
|
578
588
|
},
|
|
579
589
|
|
|
@@ -625,10 +635,17 @@ async function startInteractiveChat(initialMessage = null, options = {}) {
|
|
|
625
635
|
return;
|
|
626
636
|
}
|
|
627
637
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
638
|
+
sharedState.isBusy = true;
|
|
639
|
+
sharedState.abortController = new AbortController();
|
|
640
|
+
try {
|
|
641
|
+
await runAgentTask(initialMessage, {
|
|
642
|
+
appendMessage, streamMessage, setThinking,
|
|
643
|
+
requestApproval, askUser, setMode, appendCodeStep
|
|
644
|
+
}, sharedState);
|
|
645
|
+
} finally {
|
|
646
|
+
sharedState.isBusy = false;
|
|
647
|
+
sharedState.abortController = null;
|
|
648
|
+
}
|
|
632
649
|
}
|
|
633
650
|
}
|
|
634
651
|
|
|
@@ -45,6 +45,7 @@ const DEFAULT_CONFIG = {
|
|
|
45
45
|
apiKey: '',
|
|
46
46
|
geminiModel: 'gemini-2.5-flash',
|
|
47
47
|
language: 'th-TH',
|
|
48
|
+
assistantMode: 'chat',
|
|
48
49
|
automationBrowser: 'chromium',
|
|
49
50
|
proactiveInterval: 60, // seconds between screen captures
|
|
50
51
|
proactiveCooldown: 120, // seconds minimum between actual suggestions
|
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
const safetyManager = require('./safety_manager');
|
|
2
|
+
const { getSmartContext } = require('./smart_context');
|
|
3
|
+
|
|
4
|
+
function buildApprovalRequest(action) {
|
|
5
|
+
const classification = safetyManager.classifyAction(action);
|
|
6
|
+
if (
|
|
7
|
+
classification.tier !== safetyManager.TIERS.APPROVAL &&
|
|
8
|
+
classification.tier !== safetyManager.TIERS.DANGEROUS
|
|
9
|
+
) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
required: true,
|
|
15
|
+
tier: classification.tier,
|
|
16
|
+
reason: classification.reason,
|
|
17
|
+
action
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function executeApprovedAction(executeAction, action, clipboard) {
|
|
22
|
+
const classification = safetyManager.classifyAction(action);
|
|
23
|
+
const options = {
|
|
24
|
+
clipboard,
|
|
25
|
+
source: 'user_approved_action',
|
|
26
|
+
allowApproval: classification.tier === safetyManager.TIERS.APPROVAL,
|
|
27
|
+
allowDangerous: classification.tier === safetyManager.TIERS.DANGEROUS
|
|
28
|
+
};
|
|
29
|
+
const result = await executeAction(action, options);
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
action,
|
|
33
|
+
tier: classification.tier,
|
|
34
|
+
result,
|
|
35
|
+
message: result && typeof result === 'string'
|
|
36
|
+
? result
|
|
37
|
+
: 'Action completed.'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
1
41
|
function registerIpcHandlers({
|
|
2
42
|
app,
|
|
3
43
|
ipcMain,
|
|
@@ -36,6 +76,12 @@ function registerIpcHandlers({
|
|
|
36
76
|
|
|
37
77
|
if (aiResponse.action && aiResponse.action.type !== 'none') {
|
|
38
78
|
try {
|
|
79
|
+
const approval = buildApprovalRequest(aiResponse.action);
|
|
80
|
+
if (approval) {
|
|
81
|
+
aiResponse.approval = approval;
|
|
82
|
+
return aiResponse;
|
|
83
|
+
}
|
|
84
|
+
|
|
39
85
|
const actionResult = await executeAction(aiResponse.action, { clipboard });
|
|
40
86
|
if (actionResult && typeof actionResult === 'string') {
|
|
41
87
|
aiResponse.response += `\n\n${actionResult}`;
|
|
@@ -53,6 +99,18 @@ function registerIpcHandlers({
|
|
|
53
99
|
}
|
|
54
100
|
});
|
|
55
101
|
|
|
102
|
+
ipcMain.handle('execute-approved-action', async (event, action) => {
|
|
103
|
+
try {
|
|
104
|
+
if (!action || action.type === 'none') {
|
|
105
|
+
return { success: false, message: 'No action to execute.' };
|
|
106
|
+
}
|
|
107
|
+
return await executeApprovedAction(executeAction, action, clipboard);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('[ApprovedAction] Error:', err);
|
|
110
|
+
return { success: false, message: err.message || 'Action failed.' };
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
56
114
|
ipcMain.on('close-window', () => {
|
|
57
115
|
const mainWindow = windowManager.getMainWindow();
|
|
58
116
|
if (mainWindow) mainWindow.hide();
|
|
@@ -167,6 +225,30 @@ function registerIpcHandlers({
|
|
|
167
225
|
}
|
|
168
226
|
});
|
|
169
227
|
|
|
228
|
+
ipcMain.handle('spotlight-action', async (event, action) => {
|
|
229
|
+
const spotlightWindow = windowManager.getSpotlightWindow();
|
|
230
|
+
if (spotlightWindow) spotlightWindow.hide();
|
|
231
|
+
|
|
232
|
+
if (!action || action.type === 'none') {
|
|
233
|
+
return { success: false, message: 'No Spotlight action to execute.' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const result = await executeAction(action, {
|
|
238
|
+
clipboard,
|
|
239
|
+
source: 'spotlight'
|
|
240
|
+
});
|
|
241
|
+
return {
|
|
242
|
+
success: true,
|
|
243
|
+
action,
|
|
244
|
+
message: result && typeof result === 'string' ? result : 'Spotlight action completed.'
|
|
245
|
+
};
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.error('[SpotlightAction] Error:', err);
|
|
248
|
+
return { success: false, message: err.message || 'Spotlight action failed.' };
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
170
252
|
ipcMain.on('spotlight-resize', (event, width, height) => {
|
|
171
253
|
const spotlightWindow = windowManager.getSpotlightWindow();
|
|
172
254
|
if (spotlightWindow) spotlightWindow.setSize(width, height);
|
|
@@ -207,6 +289,8 @@ function registerIpcHandlers({
|
|
|
207
289
|
ipcMain.on('vision-cancel', () => screenCapture.cancel());
|
|
208
290
|
ipcMain.handle('capture-silent-screen', () => screenCapture.captureSilentScreen());
|
|
209
291
|
|
|
292
|
+
ipcMain.handle('get-smart-context', () => getSmartContext({ clipboard }));
|
|
293
|
+
|
|
210
294
|
ipcMain.on('toggle-proactive', (event, isOn) => {
|
|
211
295
|
if (isOn) {
|
|
212
296
|
proactiveLoop.start();
|
|
@@ -245,4 +329,4 @@ function registerIpcHandlers({
|
|
|
245
329
|
});
|
|
246
330
|
}
|
|
247
331
|
|
|
248
|
-
module.exports = { registerIpcHandlers };
|
|
332
|
+
module.exports = { registerIpcHandlers, buildApprovalRequest, executeApprovedAction };
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
|
|
4
|
+
const MAX_TEXT_LENGTH = 2000;
|
|
5
|
+
const BROWSER_NAMES = [
|
|
6
|
+
'chrome',
|
|
7
|
+
'chromium',
|
|
8
|
+
'brave',
|
|
9
|
+
'firefox',
|
|
10
|
+
'edge',
|
|
11
|
+
'safari',
|
|
12
|
+
'opera',
|
|
13
|
+
'vivaldi'
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function run(command, args = [], options = {}) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
execFile(command, args, { timeout: options.timeout || 1200 }, (error, stdout) => {
|
|
19
|
+
if (error) {
|
|
20
|
+
resolve(null);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
resolve(String(stdout || '').trim() || null);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function truncateText(value, maxLength = MAX_TEXT_LENGTH) {
|
|
29
|
+
const text = String(value || '').replace(/\0/g, '').trim();
|
|
30
|
+
if (text.length <= maxLength) return text;
|
|
31
|
+
return `${text.slice(0, maxLength)}\n[truncated ${text.length - maxLength} chars]`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeProcessName(value) {
|
|
35
|
+
return String(value || '').trim().replace(/\.exe$/i, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isBrowserProcess(name = '') {
|
|
39
|
+
const normalized = normalizeProcessName(name).toLowerCase();
|
|
40
|
+
return BROWSER_NAMES.some(browser => normalized.includes(browser));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function getLinuxActiveWindow() {
|
|
44
|
+
const windowId = await run('xdotool', ['getactivewindow']);
|
|
45
|
+
if (!windowId) return null;
|
|
46
|
+
|
|
47
|
+
const [title, pid] = await Promise.all([
|
|
48
|
+
run('xdotool', ['getwindowname', windowId]),
|
|
49
|
+
run('xdotool', ['getwindowpid', windowId])
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
let processName = '';
|
|
53
|
+
if (pid) {
|
|
54
|
+
processName = await run('ps', ['-p', pid, '-o', 'comm=']) || '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
id: windowId,
|
|
59
|
+
title: title || '',
|
|
60
|
+
appName: normalizeProcessName(processName),
|
|
61
|
+
processName: normalizeProcessName(processName),
|
|
62
|
+
pid: pid ? Number(pid) : null,
|
|
63
|
+
platform: 'linux'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function getMacActiveWindow() {
|
|
68
|
+
const script = [
|
|
69
|
+
'tell application "System Events"',
|
|
70
|
+
'set frontApp to first application process whose frontmost is true',
|
|
71
|
+
'set appName to name of frontApp',
|
|
72
|
+
'set windowTitle to ""',
|
|
73
|
+
'try',
|
|
74
|
+
'set windowTitle to name of front window of frontApp',
|
|
75
|
+
'end try',
|
|
76
|
+
'return appName & linefeed & windowTitle',
|
|
77
|
+
'end tell'
|
|
78
|
+
].join('\n');
|
|
79
|
+
const output = await run('osascript', ['-e', script]);
|
|
80
|
+
if (!output) return null;
|
|
81
|
+
const [appName = '', title = ''] = output.split(/\r?\n/);
|
|
82
|
+
return {
|
|
83
|
+
title,
|
|
84
|
+
appName,
|
|
85
|
+
processName: appName,
|
|
86
|
+
pid: null,
|
|
87
|
+
platform: 'darwin'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function getWindowsActiveWindow() {
|
|
92
|
+
const script = [
|
|
93
|
+
'Add-Type @\'',
|
|
94
|
+
'using System;',
|
|
95
|
+
'using System.Runtime.InteropServices;',
|
|
96
|
+
'using System.Text;',
|
|
97
|
+
'public class Win {',
|
|
98
|
+
'[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();',
|
|
99
|
+
'[DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);',
|
|
100
|
+
'[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);',
|
|
101
|
+
'}',
|
|
102
|
+
'\'@',
|
|
103
|
+
'$hwnd = [Win]::GetForegroundWindow()',
|
|
104
|
+
'$builder = New-Object System.Text.StringBuilder 1024',
|
|
105
|
+
'[void][Win]::GetWindowText($hwnd, $builder, $builder.Capacity)',
|
|
106
|
+
'$pid = 0',
|
|
107
|
+
'[void][Win]::GetWindowThreadProcessId($hwnd, [ref]$pid)',
|
|
108
|
+
'$proc = Get-Process -Id $pid -ErrorAction SilentlyContinue',
|
|
109
|
+
'[PSCustomObject]@{ title = $builder.ToString(); appName = $proc.ProcessName; pid = $pid } | ConvertTo-Json -Compress'
|
|
110
|
+
].join('\n');
|
|
111
|
+
const output = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { timeout: 1800 });
|
|
112
|
+
if (!output) return null;
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(output);
|
|
115
|
+
return {
|
|
116
|
+
title: parsed.title || '',
|
|
117
|
+
appName: normalizeProcessName(parsed.appName),
|
|
118
|
+
processName: normalizeProcessName(parsed.appName),
|
|
119
|
+
pid: parsed.pid ? Number(parsed.pid) : null,
|
|
120
|
+
platform: 'win32'
|
|
121
|
+
};
|
|
122
|
+
} catch (_) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function getActiveWindowContext(platform = process.platform) {
|
|
128
|
+
try {
|
|
129
|
+
if (platform === 'darwin') return await getMacActiveWindow();
|
|
130
|
+
if (platform === 'win32') return await getWindowsActiveWindow();
|
|
131
|
+
return await getLinuxActiveWindow();
|
|
132
|
+
} catch (_) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function getLinuxSelectedText() {
|
|
138
|
+
const attempts = [
|
|
139
|
+
['wl-paste', ['--primary', '--no-newline']],
|
|
140
|
+
['xclip', ['-selection', 'primary', '-out']],
|
|
141
|
+
['xsel', ['--primary', '--output']]
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
for (const [command, args] of attempts) {
|
|
145
|
+
const text = await run(command, args);
|
|
146
|
+
if (text) return truncateText(text);
|
|
147
|
+
}
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function getSelectedText(platform = process.platform) {
|
|
152
|
+
if (platform === 'linux') return getLinuxSelectedText();
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function getMacBrowserContext(appName) {
|
|
157
|
+
const normalized = String(appName || '').toLowerCase();
|
|
158
|
+
let script = '';
|
|
159
|
+
if (normalized.includes('safari')) {
|
|
160
|
+
script = 'tell application "Safari" to return name of front document & linefeed & URL of front document';
|
|
161
|
+
} else if (normalized.includes('chrome') || normalized.includes('chromium') || normalized.includes('brave') || normalized.includes('edge')) {
|
|
162
|
+
script = `tell application "${appName}" to return title of active tab of front window & linefeed & URL of active tab of front window`;
|
|
163
|
+
}
|
|
164
|
+
if (!script) return null;
|
|
165
|
+
const output = await run('osascript', ['-e', script], { timeout: 1500 });
|
|
166
|
+
if (!output) return null;
|
|
167
|
+
const [title = '', url = ''] = output.split(/\r?\n/);
|
|
168
|
+
return { title, url };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function getBrowserContext(activeWindow, platform = process.platform) {
|
|
172
|
+
if (!activeWindow || !isBrowserProcess(activeWindow.appName || activeWindow.processName)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
if (platform === 'darwin') {
|
|
176
|
+
const browser = await getMacBrowserContext(activeWindow.appName);
|
|
177
|
+
if (browser) return browser;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
title: activeWindow.title || '',
|
|
181
|
+
url: '',
|
|
182
|
+
urlUnavailableReason: 'Browser URL is not available from the current OS context without browser integration.'
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function getSmartContext(options = {}) {
|
|
187
|
+
const platform = options.platform || process.platform;
|
|
188
|
+
const clipboard = options.clipboard || null;
|
|
189
|
+
const [activeWindow, selectedText] = await Promise.all([
|
|
190
|
+
getActiveWindowContext(platform),
|
|
191
|
+
getSelectedText(platform)
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
let clipboardText = '';
|
|
195
|
+
try {
|
|
196
|
+
clipboardText = clipboard && typeof clipboard.readText === 'function'
|
|
197
|
+
? truncateText(clipboard.readText())
|
|
198
|
+
: '';
|
|
199
|
+
} catch (_) {
|
|
200
|
+
clipboardText = '';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const browser = await getBrowserContext(activeWindow, platform);
|
|
204
|
+
return {
|
|
205
|
+
capturedAt: new Date().toISOString(),
|
|
206
|
+
platform,
|
|
207
|
+
host: os.hostname(),
|
|
208
|
+
activeWindow,
|
|
209
|
+
currentApp: activeWindow ? {
|
|
210
|
+
name: activeWindow.appName || activeWindow.processName || '',
|
|
211
|
+
processName: activeWindow.processName || activeWindow.appName || '',
|
|
212
|
+
pid: activeWindow.pid || null
|
|
213
|
+
} : null,
|
|
214
|
+
browser,
|
|
215
|
+
selectedText,
|
|
216
|
+
clipboardText
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
getSmartContext,
|
|
222
|
+
getActiveWindowContext,
|
|
223
|
+
getBrowserContext,
|
|
224
|
+
getSelectedText,
|
|
225
|
+
truncateText,
|
|
226
|
+
isBrowserProcess
|
|
227
|
+
};
|
|
@@ -2,6 +2,7 @@ const { contextBridge, ipcRenderer } = require('electron');
|
|
|
2
2
|
|
|
3
3
|
contextBridge.exposeInMainWorld('spotlightAPI', {
|
|
4
4
|
submit: (query) => ipcRenderer.send('spotlight-submit', query),
|
|
5
|
+
executeAction: (action) => ipcRenderer.invoke('spotlight-action', action),
|
|
5
6
|
close: () => ipcRenderer.send('spotlight-close'),
|
|
6
7
|
hide: () => ipcRenderer.send('spotlight-hide'),
|
|
7
8
|
resize: (width, height) => ipcRenderer.send('spotlight-resize', width, height),
|