@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.
@@ -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: 'Agent powering down. Goodbye!',
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
- await runAgentTask(initialMessage, {
629
- appendMessage, streamMessage, setThinking,
630
- requestApproval, askUser, setMode, appendCodeStep
631
- }, sharedState);
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),