@pheem49/mint 1.5.3 → 1.5.4
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 +3 -2
- package/preload.js +2 -0
- package/src/AI_Brain/Gemini_API.js +52 -22
- package/src/Automation_Layer/file_operations.js +17 -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 +197 -3
package/GUIDE_TH.md
CHANGED
|
@@ -6,15 +6,27 @@
|
|
|
6
6
|
|
|
7
7
|
## 1. การติดตั้ง (Installation)
|
|
8
8
|
|
|
9
|
-
คุณสามารถติดตั้ง Mint
|
|
9
|
+
คุณสามารถติดตั้ง Mint ได้ด้วยวิธีต่างๆ ดังนี้:
|
|
10
10
|
|
|
11
|
-
### วิธีที่ 1:
|
|
12
|
-
|
|
11
|
+
### วิธีที่ 1: ติดตั้งแบบด่วน (แนะนำ)
|
|
12
|
+
วิธีนี้จะติดตั้ง Mint CLI ให้อัตโนมัติในคำสั่งเดียว:
|
|
13
|
+
**สำหรับ macOS และ Linux:**
|
|
14
|
+
```bash
|
|
15
|
+
curl -fsSL https://raw.githubusercontent.com/Pheem49/Mint/main/install.sh | bash
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**สำหรับ Windows (PowerShell หรือ CMD):**
|
|
19
|
+
```powershell
|
|
20
|
+
powershell -Command "iwr -useb https://raw.githubusercontent.com/Pheem49/Mint/main/install.ps1 | iex"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### วิธีที่ 2: ติดตั้งผ่าน NPM
|
|
24
|
+
หากคุณต้องการใช้งานคำสั่ง `mint` และถนัดการใช้ NPM:
|
|
13
25
|
```bash
|
|
14
26
|
npm install -g @pheem49/mint@latest
|
|
15
27
|
```
|
|
16
28
|
|
|
17
|
-
### วิธีที่
|
|
29
|
+
### วิธีที่ 3: ติดตั้งจาก Source Code (สำหรับนักพัฒนา)
|
|
18
30
|
หากคุณต้องการแก้ไขโค้ดหรือลองฟีเจอร์ใหม่ๆ:
|
|
19
31
|
1. Clone repository: `git clone https://github.com/Pheem49/Mint.git`
|
|
20
32
|
2. เข้าไปที่โฟลเดอร์: `cd Mint`
|
package/README.md
CHANGED
|
@@ -39,7 +39,23 @@ Mint is an AI assistant built to live in your desktop and terminal. It combines
|
|
|
39
39
|
|
|
40
40
|
## Installation & Setup
|
|
41
41
|
|
|
42
|
-
###
|
|
42
|
+
### Quick Install (Recommended)
|
|
43
|
+
|
|
44
|
+
The easiest way to install Mint CLI is using our installation script:
|
|
45
|
+
|
|
46
|
+
**For macOS & Linux:**
|
|
47
|
+
```bash
|
|
48
|
+
curl -fsSL https://raw.githubusercontent.com/Pheem49/Mint/main/install.sh | bash
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**For Windows (PowerShell or CMD):**
|
|
52
|
+
```powershell
|
|
53
|
+
powershell -Command "iwr -useb https://raw.githubusercontent.com/Pheem49/Mint/main/install.ps1 | iex"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### NPM Install
|
|
57
|
+
|
|
58
|
+
If you prefer to install via NPM directly:
|
|
43
59
|
|
|
44
60
|
```bash
|
|
45
61
|
npm install -g @pheem49/mint@latest
|
package/install.ps1
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Mint CLI Installation Script for Windows
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
Installs the Mint CLI package globally via NPM.
|
|
6
|
+
#>
|
|
7
|
+
|
|
8
|
+
$ErrorActionPreference = "Stop"
|
|
9
|
+
|
|
10
|
+
Write-Host "Starting Mint CLI installation..." -ForegroundColor Cyan
|
|
11
|
+
|
|
12
|
+
# 1. Check for Node.js
|
|
13
|
+
try {
|
|
14
|
+
$nodeVersion = (node -v) 2>$null
|
|
15
|
+
if ([string]::IsNullOrWhiteSpace($nodeVersion)) {
|
|
16
|
+
Write-Host "[Error] Node.js is not found on your system!" -ForegroundColor Red
|
|
17
|
+
Write-Host "Please install Node.js (version 18 or higher) before using Mint CLI." -ForegroundColor Yellow
|
|
18
|
+
Write-Host "Download at: https://nodejs.org/"
|
|
19
|
+
exit 1
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
Write-Host "[Error] Node.js is not found on your system!" -ForegroundColor Red
|
|
23
|
+
Write-Host "Please install Node.js (version 18 or higher) before using Mint CLI." -ForegroundColor Yellow
|
|
24
|
+
Write-Host "Download at: https://nodejs.org/"
|
|
25
|
+
exit 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# 2. Check for NPM
|
|
29
|
+
try {
|
|
30
|
+
$npmVersion = (npm -v) 2>$null
|
|
31
|
+
if ([string]::IsNullOrWhiteSpace($npmVersion)) {
|
|
32
|
+
Write-Host "[Error] NPM is not found on your system!" -ForegroundColor Red
|
|
33
|
+
exit 1
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
Write-Host "[Error] NPM is not found on your system!" -ForegroundColor Red
|
|
37
|
+
exit 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Check Node.js version (recommend 18+)
|
|
41
|
+
$majorVersion = [int]($nodeVersion.Substring(1).Split('.')[0])
|
|
42
|
+
if ($majorVersion -lt 18) {
|
|
43
|
+
Write-Host "[Warning] Your Node.js version is too old (current: $nodeVersion)" -ForegroundColor Yellow
|
|
44
|
+
Write-Host "It is recommended to update to version 18 or higher for full functionality." -ForegroundColor Yellow
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# 3. Install Mint CLI via NPM
|
|
48
|
+
Write-Host "Downloading and installing @pheem49/mint..." -ForegroundColor Cyan
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
# In Windows, we usually don't need sudo for global npm install if Node was installed for the user.
|
|
52
|
+
# We will just run the command normally.
|
|
53
|
+
npm install -g @pheem49/mint@latest
|
|
54
|
+
} catch {
|
|
55
|
+
Write-Host "[Error] Installation failed. Try running PowerShell as Administrator." -ForegroundColor Red
|
|
56
|
+
exit 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Write-Host ""
|
|
60
|
+
Write-Host "Mint CLI installed successfully!" -ForegroundColor Green
|
|
61
|
+
Write-Host "- Run 'mint' to get started."
|
|
62
|
+
Write-Host "- Run 'mint onboard' to set up your API Keys."
|
|
63
|
+
Write-Host "----------------------------------------"
|
|
64
|
+
Write-Host "Happy coding!" -ForegroundColor Cyan
|
package/install.sh
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Mint CLI Installation Script
|
|
3
|
+
# Usage: curl -fsSL <URL>/install.sh | bash
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
echo "Starting Mint CLI installation..."
|
|
8
|
+
|
|
9
|
+
# 1. Check for Node.js
|
|
10
|
+
if ! command -v node &> /dev/null
|
|
11
|
+
then
|
|
12
|
+
echo "[Error] Node.js is not found on your system!"
|
|
13
|
+
echo "Please install Node.js (version 18 or higher) before using Mint CLI."
|
|
14
|
+
echo "Download at: https://nodejs.org/"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# 2. Check for NPM
|
|
19
|
+
if ! command -v npm &> /dev/null
|
|
20
|
+
then
|
|
21
|
+
echo "[Error] NPM is not found on your system!"
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Check Node.js version (recommend 18+)
|
|
26
|
+
NODE_VERSION=$(node -v | cut -d 'v' -f 2 | cut -d '.' -f 1)
|
|
27
|
+
if [ "$NODE_VERSION" -lt 18 ]; then
|
|
28
|
+
echo "[Warning] Your Node.js version is too old (current: v${NODE_VERSION})"
|
|
29
|
+
echo "It is recommended to update to version 18 or higher for full functionality."
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# 3. Install Mint CLI via NPM
|
|
33
|
+
echo "Downloading and installing @pheem49/mint (may require Admin/Root privileges)..."
|
|
34
|
+
|
|
35
|
+
# Determine if we need sudo
|
|
36
|
+
if [ "$EUID" -ne 0 ] && command -v sudo &> /dev/null; then
|
|
37
|
+
# Check if npm global directory is writable by the current user (e.g., using NVM)
|
|
38
|
+
NPM_PREFIX=$(npm config get prefix 2>/dev/null || echo "/usr/local")
|
|
39
|
+
if [ -w "$NPM_PREFIX" ] || [ -w "$NPM_PREFIX/lib/node_modules" ]; then
|
|
40
|
+
npm install -g @pheem49/mint@latest
|
|
41
|
+
else
|
|
42
|
+
# Preserve user's PATH so sudo can find npm if it's installed via custom paths
|
|
43
|
+
sudo env "PATH=$PATH" npm install -g @pheem49/mint@latest
|
|
44
|
+
fi
|
|
45
|
+
else
|
|
46
|
+
npm install -g @pheem49/mint@latest
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
echo ""
|
|
50
|
+
echo "Mint CLI installed successfully!"
|
|
51
|
+
echo "- Run 'mint' to get started."
|
|
52
|
+
echo "- Run 'mint onboard' to set up your API Keys."
|
|
53
|
+
echo "----------------------------------------"
|
|
54
|
+
echo "Happy coding!"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pheem49/mint",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"description": "A powerful Electron-based AI desktop assistant powered by Google Gemini, featuring screen vision, web automation, and proactive suggestions.",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"scripts": {
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"ink-text-input": "^6.0.0",
|
|
47
47
|
"mammoth": "^1.12.0",
|
|
48
48
|
"pdf-parse": "^2.4.5",
|
|
49
|
-
"react": "^19.2.5"
|
|
49
|
+
"react": "^19.2.5",
|
|
50
|
+
"read-excel-file": "^9.0.10"
|
|
50
51
|
},
|
|
51
52
|
"peerDependenciesOptional": {
|
|
52
53
|
"puppeteer": ">=22.0.0",
|
package/preload.js
CHANGED
|
@@ -25,12 +25,14 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
25
25
|
startVision: () => ipcRenderer.invoke('start-screen-capture'),
|
|
26
26
|
onVisionReady: (callback) => ipcRenderer.on('vision-ready', (event, data) => callback(data)),
|
|
27
27
|
captureSilentScreen: () => ipcRenderer.invoke('capture-silent-screen'),
|
|
28
|
+
getSmartContext: () => ipcRenderer.invoke('get-smart-context'),
|
|
28
29
|
// Proactive Assistant
|
|
29
30
|
onProactiveSuggestion: (callback) => ipcRenderer.on('proactive-suggestion', (event, data) => callback(data)),
|
|
30
31
|
onProactiveNotification: (callback) => ipcRenderer.on('proactive-notification', (event, data) => callback(data)),
|
|
31
32
|
toggleProactive: (isOn) => ipcRenderer.send('toggle-proactive', isOn),
|
|
32
33
|
recordBehavior: (context) => ipcRenderer.send('record-behavior', context),
|
|
33
34
|
executeProactiveAction: (action) => ipcRenderer.invoke('execute-proactive-action', action),
|
|
35
|
+
executeApprovedAction: (action) => ipcRenderer.invoke('execute-approved-action', action),
|
|
34
36
|
onSpotlightToChat: (callback) => ipcRenderer.on('spotlight-to-chat', (_event, query) => callback(query)),
|
|
35
37
|
notifyAiResponse: () => ipcRenderer.send('ai-notify'),
|
|
36
38
|
clearAiNotifications: () => ipcRenderer.send('ai-notify-clear'),
|
|
@@ -47,6 +47,34 @@ function normalizeImageList(base64Image) {
|
|
|
47
47
|
return Array.isArray(base64Image) ? base64Image.filter(Boolean) : [base64Image];
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const CHAT_MODE_ACTION_POLICY = `GOAL:
|
|
51
|
+
Your goal is to help the user with their queries. This Electron app is Chat Mode: use at most ONE simple action per user message, only when the latest message explicitly asks for that local action. If the user asks a question or asks you to provide text/commands, answer with action "none".
|
|
52
|
+
|
|
53
|
+
ACTION DISCIPLINE:
|
|
54
|
+
- Always return a single JSON object. Never return a JSON array or multiple actions.
|
|
55
|
+
- If the user asks "พิมพ์คำสั่งให้หน่อย", "บอกคำสั่ง", "ขอคำสั่ง", "what command", or "type the command", provide the command in "response" and set action "none". Do NOT use "type_text" or "key_tap".
|
|
56
|
+
- Use "type_text", "key_tap", "mouse_click", or "mouse_move" only when the user explicitly asks you to control the currently focused UI, not when they ask for a command to copy/type themselves.
|
|
57
|
+
- If the user asks to run terminal commands or code, Chat Mode should provide the command or tell them to use the Mint CLI agent. Do not type or press Enter on their behalf.
|
|
58
|
+
- Never say you opened, checked, inspected, or verified a file/folder unless the selected action actually does it and the app will execute that action.
|
|
59
|
+
- If the request needs workspace code inspection, edits, tests, or shell execution, tell the user to use the Mint CLI agent instead of pretending to inspect files.`;
|
|
60
|
+
|
|
61
|
+
const AGENT_MODE_ACTION_POLICY = `GOAL:
|
|
62
|
+
Your goal is to act as Mint's Desktop Agent Mode. You may use ONE concrete desktop action per response when it directly advances the user's latest request or a clear desktop task implied by Smart Context. Prefer useful action over explaining when the user asked Mint to do something.
|
|
63
|
+
|
|
64
|
+
ACTION DISCIPLINE:
|
|
65
|
+
- Always return a single JSON object. Never return a JSON array or multiple actions.
|
|
66
|
+
- Choose exactly one action when a desktop action is useful and the user's intent is clear; otherwise use action "none" and ask a concise follow-up.
|
|
67
|
+
- You may use safe desktop actions such as open_url, search, open_app, find_path, open_file, open_folder, create_folder, clipboard_write, learn_file, learn_folder, plugin, mcp_tool, web_automation, system_info, mouse_move, mouse_click, type_text, and key_tap when they match the request.
|
|
68
|
+
- Approval and dangerous actions are handled by Mint's UI. You may propose system_automation or delete_file only when the user clearly requested it; the app will ask for permission before running.
|
|
69
|
+
- For UI-control actions (mouse_click, mouse_move, type_text, key_tap), rely on Smart Context or the attached screenshot. If the target is ambiguous, ask before acting.
|
|
70
|
+
- If the user asks "พิมพ์คำสั่งให้หน่อย", "บอกคำสั่ง", "ขอคำสั่ง", "what command", or "type the command", provide the command in "response" and set action "none" unless they explicitly ask Mint to type it into the active UI.
|
|
71
|
+
- If the request needs workspace code inspection, edits, tests, or shell execution, tell the user to use the Mint CLI agent instead of pretending to inspect files or run commands from Chat UI.
|
|
72
|
+
- Never say you opened, checked, inspected, or verified something unless the selected action actually does it and the app will execute that action.`;
|
|
73
|
+
|
|
74
|
+
function buildActionModeInstruction(config = readConfig()) {
|
|
75
|
+
return config.assistantMode === 'agent' ? AGENT_MODE_ACTION_POLICY : CHAT_MODE_ACTION_POLICY;
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
const systemInstruction = `You are "Mint" (มิ้นท์), a cute, cheerful, and highly helpful female Local AI Desktop Agent.
|
|
51
79
|
|
|
52
80
|
PERSONALITY & TONE:
|
|
@@ -71,16 +99,7 @@ NATURAL CHAT FLOW:
|
|
|
71
99
|
- The latest user message is authoritative. Do not continue or describe older tasks unless the latest message explicitly asks you to continue them.
|
|
72
100
|
- For greetings, name-calls, acknowledgements, and backchannels such as "มิ้น", "มิ้นๆ", "อ๋อ", "โอเค", "ขอบคุณ", "hi", "hello", "ok", or "thanks", return action "none" and a short reply only.
|
|
73
101
|
|
|
74
|
-
|
|
75
|
-
Your goal is to help the user with their queries. This Electron app is Chat Mode, not Code Agent Mode: use at most ONE simple action per user message, only when the latest message explicitly asks for that local action. If the user asks a question or asks you to provide text/commands, answer with action "none".
|
|
76
|
-
|
|
77
|
-
ACTION DISCIPLINE:
|
|
78
|
-
- Always return a single JSON object. Never return a JSON array or multiple actions.
|
|
79
|
-
- If the user asks "พิมพ์คำสั่งให้หน่อย", "บอกคำสั่ง", "ขอคำสั่ง", "what command", or "type the command", provide the command in "response" and set action "none". Do NOT use "type_text" or "key_tap".
|
|
80
|
-
- Use "type_text", "key_tap", "mouse_click", or "mouse_move" only when the user explicitly asks you to control the currently focused UI, not when they ask for a command to copy/type themselves.
|
|
81
|
-
- If the user asks to run terminal commands or code, Chat Mode should provide the command or tell them to use the CLI agent. Do not type or press Enter on their behalf.
|
|
82
|
-
- Never say you opened, checked, inspected, or verified a file/folder unless the selected action actually does it and the app will execute that action.
|
|
83
|
-
- If the request needs workspace code inspection, edits, tests, or shell execution, tell the user to use the Mint CLI agent instead of pretending to inspect files.
|
|
102
|
+
{{ACTION_MODE_INSTRUCTION}}
|
|
84
103
|
|
|
85
104
|
CREATOR INFO:
|
|
86
105
|
- The creator is Pheem49.
|
|
@@ -144,6 +163,7 @@ ${toolRegistry.buildToolPromptSection()}
|
|
|
144
163
|
// Replaces 5 previously duplicated mcpPrompt blocks.
|
|
145
164
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
146
165
|
function buildSystemPrompt() {
|
|
166
|
+
const config = readConfig();
|
|
147
167
|
pluginManager.loadPlugins();
|
|
148
168
|
const mcpTools = mcpManager.getAllTools();
|
|
149
169
|
|
|
@@ -171,7 +191,9 @@ function buildSystemPrompt() {
|
|
|
171
191
|
workspaceSection = `\n\n[WORKSPACE DETECTED: ${ws.name}]\nPath: ${ws.path}\nProject Instructions: ${ws.instructions}\n`;
|
|
172
192
|
}
|
|
173
193
|
|
|
174
|
-
|
|
194
|
+
const modeInstruction = buildActionModeInstruction(config);
|
|
195
|
+
const baseInstruction = systemInstruction.replace('{{ACTION_MODE_INSTRUCTION}}', modeInstruction);
|
|
196
|
+
return baseInstruction + personaInstruction + workspaceSection + pluginManager.getPromptDescriptions() + mcpSection + userContext;
|
|
175
197
|
}
|
|
176
198
|
|
|
177
199
|
function buildMessageWithRelevantMemory(finalMessage) {
|
|
@@ -197,10 +219,15 @@ function stripRelevantMemoryBlock(text) {
|
|
|
197
219
|
return input
|
|
198
220
|
.replace(/\n?\[Relevant long-term memory for this user message\][\s\S]*?\[End relevant memory\]\n?/g, '\n')
|
|
199
221
|
.replace(/^\s*\[Relevant long-term memory for this user message\][\s\S]*?\[End relevant memory\]\s*/g, '')
|
|
222
|
+
.replace(/\n?\[SMART_CONTEXT\][\s\S]*?\[\/SMART_CONTEXT\]\n?/g, '\n')
|
|
200
223
|
.replace(/\n?\[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER\][\s\S]*/g, '')
|
|
201
224
|
.trim();
|
|
202
225
|
}
|
|
203
226
|
|
|
227
|
+
function hasSmartContextBlock(text) {
|
|
228
|
+
return /\[SMART_CONTEXT\][\s\S]*?\[\/SMART_CONTEXT\]/.test(String(text || ''));
|
|
229
|
+
}
|
|
230
|
+
|
|
204
231
|
function cleanHistoryForStorage(history) {
|
|
205
232
|
if (!Array.isArray(history)) return [];
|
|
206
233
|
return history.map(msg => ({
|
|
@@ -467,13 +494,15 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
467
494
|
const config = readConfig();
|
|
468
495
|
const images = normalizeImageList(base64Image);
|
|
469
496
|
const previousHistory = readChatHistory();
|
|
497
|
+
const userVisibleMessage = stripRelevantMemoryBlock(message);
|
|
498
|
+
const containsSmartContext = hasSmartContextBlock(message);
|
|
470
499
|
|
|
471
500
|
let finalMessage = message;
|
|
472
501
|
|
|
473
502
|
// Inject Local RAG Context
|
|
474
|
-
if (
|
|
503
|
+
if (userVisibleMessage && userVisibleMessage.trim().length > 0 && shouldUseKnowledgeSearch(userVisibleMessage)) {
|
|
475
504
|
const { searchKnowledge } = require('./knowledge_base');
|
|
476
|
-
const retrievedDocs = await searchKnowledge(
|
|
505
|
+
const retrievedDocs = await searchKnowledge(userVisibleMessage);
|
|
477
506
|
if (retrievedDocs && retrievedDocs.length > 0) {
|
|
478
507
|
let contextString = `\n\n[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER]\n`;
|
|
479
508
|
retrievedDocs.forEach(doc => {
|
|
@@ -483,8 +512,8 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
483
512
|
}
|
|
484
513
|
}
|
|
485
514
|
|
|
486
|
-
if (
|
|
487
|
-
const cached = memoryStore.getCachedResponse(
|
|
515
|
+
if (!containsSmartContext && userVisibleMessage && images.length === 0 && !base64Audio) {
|
|
516
|
+
const cached = memoryStore.getCachedResponse(userVisibleMessage);
|
|
488
517
|
if (cached) return cached;
|
|
489
518
|
}
|
|
490
519
|
|
|
@@ -507,15 +536,15 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
507
536
|
model: getProviderModel(provider, config),
|
|
508
537
|
usage: client.getUsageSummary()
|
|
509
538
|
};
|
|
510
|
-
const parsedResult = parseChatProviderResponse(outputText, finalMessage, now);
|
|
539
|
+
const parsedResult = parseChatProviderResponse(outputText, userVisibleMessage || finalMessage, now);
|
|
511
540
|
parsedResult.providerInfo = providerInfo;
|
|
512
|
-
appendChatProviderHistory(previousHistory, finalMessage, outputText, providerInfo, now);
|
|
541
|
+
appendChatProviderHistory(previousHistory, userVisibleMessage || finalMessage, outputText, providerInfo, now);
|
|
513
542
|
|
|
514
|
-
if (finalMessage && parsedResult.response) {
|
|
543
|
+
if ((userVisibleMessage || finalMessage) && parsedResult.response) {
|
|
515
544
|
setImmediate(() => {
|
|
516
|
-
memoryStore.recordInteraction(finalMessage, parsedResult.response);
|
|
517
|
-
if (images.length === 0 && !base64Audio) {
|
|
518
|
-
memoryStore.cacheResponse(finalMessage, parsedResult);
|
|
545
|
+
memoryStore.recordInteraction(userVisibleMessage || finalMessage, parsedResult.response);
|
|
546
|
+
if (!containsSmartContext && images.length === 0 && !base64Audio) {
|
|
547
|
+
memoryStore.cacheResponse(userVisibleMessage || finalMessage, parsedResult);
|
|
519
548
|
}
|
|
520
549
|
});
|
|
521
550
|
}
|
|
@@ -761,6 +790,7 @@ module.exports = {
|
|
|
761
790
|
refreshApiKeyFromConfig,
|
|
762
791
|
_helpers: {
|
|
763
792
|
getProviderAttemptOrder,
|
|
764
|
-
normalizeParsedResult
|
|
793
|
+
normalizeParsedResult,
|
|
794
|
+
buildActionModeInstruction
|
|
765
795
|
}
|
|
766
796
|
};
|
|
@@ -18,6 +18,8 @@ const IGNORED_DIRECTORY_NAMES = new Set([
|
|
|
18
18
|
'coverage'
|
|
19
19
|
]);
|
|
20
20
|
|
|
21
|
+
const COMMON_HOME_FOLDERS = ['Downloads', 'Desktop', 'Documents', 'Videos', 'Pictures', 'Music', 'vscode', 'Games'];
|
|
22
|
+
|
|
21
23
|
function getSearchRoots() {
|
|
22
24
|
return Array.from(new Set([
|
|
23
25
|
process.cwd(),
|
|
@@ -34,8 +36,6 @@ function resolveSmartPath(target) {
|
|
|
34
36
|
if (!target) return target;
|
|
35
37
|
|
|
36
38
|
const home = os.homedir();
|
|
37
|
-
const commonFolders = ['Downloads', 'Desktop', 'Documents', 'Videos', 'Pictures', 'Music', 'vscode', 'Games'];
|
|
38
|
-
|
|
39
39
|
// 1. If it's already an absolute path and exists, use it
|
|
40
40
|
if (path.isAbsolute(target) && fs.existsSync(target)) return target;
|
|
41
41
|
|
|
@@ -54,13 +54,13 @@ function resolveSmartPath(target) {
|
|
|
54
54
|
// 4. Check if the target itself starts with a common folder (e.g., "Downloads/resume.pdf")
|
|
55
55
|
const parts = target.split(/[/\\]/);
|
|
56
56
|
const firstPart = parts[0];
|
|
57
|
-
if (
|
|
57
|
+
if (COMMON_HOME_FOLDERS.includes(firstPart)) {
|
|
58
58
|
const potentialPath = path.join(home, target);
|
|
59
59
|
if (fs.existsSync(potentialPath)) return potentialPath;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// 5. Try searching the filename in all common folders
|
|
63
|
-
for (const folder of
|
|
63
|
+
for (const folder of COMMON_HOME_FOLDERS) {
|
|
64
64
|
const potentialPath = path.join(home, folder, target);
|
|
65
65
|
if (fs.existsSync(potentialPath)) return potentialPath;
|
|
66
66
|
}
|
|
@@ -76,6 +76,18 @@ function findPath(target, options = {}) {
|
|
|
76
76
|
|
|
77
77
|
const normalizedType = ['file', 'dir', 'any'].includes(options.type) ? options.type : 'any';
|
|
78
78
|
const loweredQuery = target.trim().toLowerCase();
|
|
79
|
+
const directPath = resolveSmartPath(target.trim());
|
|
80
|
+
if (directPath && path.isAbsolute(directPath) && fs.existsSync(directPath)) {
|
|
81
|
+
const stat = fs.statSync(directPath);
|
|
82
|
+
const directType = stat.isDirectory() ? 'dir' : 'file';
|
|
83
|
+
if (normalizedType === 'any' || normalizedType === directType) {
|
|
84
|
+
return {
|
|
85
|
+
success: true,
|
|
86
|
+
matches: [{ path: directPath, type: directType }]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
const exactMatches = [];
|
|
80
92
|
const partialMatches = [];
|
|
81
93
|
const visited = new Set();
|
|
@@ -271,4 +283,4 @@ async function deleteFile(target) {
|
|
|
271
283
|
}
|
|
272
284
|
}
|
|
273
285
|
|
|
274
|
-
module.exports = { createFolder, openFile, deleteFile, findPath };
|
|
286
|
+
module.exports = { createFolder, openFile, deleteFile, findPath, resolveSmartPath };
|
|
@@ -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 };
|