@pheem49/mint 1.4.0 → 1.4.2
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/.codex +0 -0
- package/README.md +101 -148
- package/main.js +21 -1
- package/mint-cli-logic.js +23 -8
- package/mint-cli.js +223 -137
- package/package.json +1 -1
- package/src/AI_Brain/Gemini_API.js +38 -24
- package/src/AI_Brain/agent_orchestrator.js +6 -6
- package/src/AI_Brain/proactive_engine.js +2 -8
- package/src/Automation_Layer/file_operations.js +136 -6
- package/src/Automation_Layer/open_app.js +72 -43
- package/src/Automation_Layer/open_website.js +3 -3
- package/src/CLI/chat_router.js +70 -24
- package/src/CLI/chat_ui.js +197 -44
- package/src/CLI/code_agent.js +337 -93
- package/src/CLI/list_features.js +3 -1
- package/src/CLI/workspace_manager.js +15 -6
- package/src/Plugins/docker.js +12 -10
- package/src/System/config_manager.js +1 -1
- package/src/System/custom_workflows.js +9 -2
- package/tests/chat_router.test.js +42 -0
- package/tests/code_agent.test.js +69 -0
- package/tests/docker.test.js +46 -0
- package/tests/file_operations.test.js +57 -0
- package/tests/provider_routing.test.js +67 -0
- package/tests/workspace_manager.test.js +15 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
2
|
let shell;
|
|
3
3
|
try {
|
|
4
4
|
shell = require('electron').shell;
|
|
@@ -9,6 +9,22 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
11
|
|
|
12
|
+
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
13
|
+
'.git',
|
|
14
|
+
'node_modules',
|
|
15
|
+
'.cache',
|
|
16
|
+
'dist',
|
|
17
|
+
'build',
|
|
18
|
+
'coverage'
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function getSearchRoots() {
|
|
22
|
+
return Array.from(new Set([
|
|
23
|
+
process.cwd(),
|
|
24
|
+
os.homedir()
|
|
25
|
+
]));
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
/**
|
|
13
29
|
* Smartly resolves a path.
|
|
14
30
|
* If a path starts with '/' but doesn't exist at root, checks if it exists relative to home.
|
|
@@ -53,6 +69,109 @@ function resolveSmartPath(target) {
|
|
|
53
69
|
return target;
|
|
54
70
|
}
|
|
55
71
|
|
|
72
|
+
function findPath(target, options = {}) {
|
|
73
|
+
if (!target || !target.trim()) {
|
|
74
|
+
return { success: false, message: 'No search query provided.', matches: [] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const normalizedType = ['file', 'dir', 'any'].includes(options.type) ? options.type : 'any';
|
|
78
|
+
const loweredQuery = target.trim().toLowerCase();
|
|
79
|
+
const exactMatches = [];
|
|
80
|
+
const partialMatches = [];
|
|
81
|
+
const visited = new Set();
|
|
82
|
+
const maxResults = options.maxResults || 20;
|
|
83
|
+
const searchRoots = Array.isArray(options.roots) && options.roots.length > 0
|
|
84
|
+
? options.roots
|
|
85
|
+
: getSearchRoots();
|
|
86
|
+
|
|
87
|
+
function buildMatch(entryPath, entryType, rootPath, exactNameMatch) {
|
|
88
|
+
const relativeToCwd = path.relative(process.cwd(), entryPath);
|
|
89
|
+
const pathDepth = entryPath.split(path.sep).length;
|
|
90
|
+
return {
|
|
91
|
+
path: entryPath,
|
|
92
|
+
type: entryType,
|
|
93
|
+
exactNameMatch,
|
|
94
|
+
inCurrentWorkspace: !relativeToCwd.startsWith('..') && !path.isAbsolute(relativeToCwd),
|
|
95
|
+
pathDepth,
|
|
96
|
+
rootPath
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sortMatches(matches) {
|
|
101
|
+
return matches.sort((a, b) => {
|
|
102
|
+
if (a.exactNameMatch !== b.exactNameMatch) return a.exactNameMatch ? -1 : 1;
|
|
103
|
+
if (a.inCurrentWorkspace !== b.inCurrentWorkspace) return a.inCurrentWorkspace ? -1 : 1;
|
|
104
|
+
if (a.pathDepth !== b.pathDepth) return a.pathDepth - b.pathDepth;
|
|
105
|
+
return a.path.localeCompare(b.path);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function visit(currentPath, rootPath) {
|
|
110
|
+
let entries = [];
|
|
111
|
+
try {
|
|
112
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
113
|
+
} catch (_) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const absoluteEntryPath = path.join(currentPath, entry.name);
|
|
119
|
+
if (visited.has(absoluteEntryPath)) continue;
|
|
120
|
+
visited.add(absoluteEntryPath);
|
|
121
|
+
|
|
122
|
+
const entryType = entry.isDirectory() ? 'dir' : 'file';
|
|
123
|
+
if (entry.isDirectory() && IGNORED_DIRECTORY_NAMES.has(entry.name)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const relativePath = path.relative(rootPath, absoluteEntryPath);
|
|
127
|
+
const searchablePath = relativePath || entry.name;
|
|
128
|
+
const matchesType = normalizedType === 'any' || normalizedType === entryType;
|
|
129
|
+
const lowerEntryName = entry.name.toLowerCase();
|
|
130
|
+
const exactNameMatch = lowerEntryName === loweredQuery;
|
|
131
|
+
const partialMatch = lowerEntryName.includes(loweredQuery) || searchablePath.toLowerCase().includes(loweredQuery);
|
|
132
|
+
|
|
133
|
+
if (matchesType && partialMatch) {
|
|
134
|
+
const match = buildMatch(absoluteEntryPath, entryType, rootPath, exactNameMatch);
|
|
135
|
+
if (exactNameMatch) {
|
|
136
|
+
exactMatches.push(match);
|
|
137
|
+
if (exactMatches.length >= maxResults) return;
|
|
138
|
+
} else if (exactMatches.length === 0) {
|
|
139
|
+
partialMatches.push(match);
|
|
140
|
+
if (partialMatches.length >= maxResults) return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (entry.isDirectory() && exactMatches.length < maxResults && partialMatches.length < maxResults) {
|
|
145
|
+
visit(absoluteEntryPath, rootPath);
|
|
146
|
+
if (exactMatches.length >= maxResults || partialMatches.length >= maxResults) return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const rootPath of searchRoots) {
|
|
152
|
+
if (!fs.existsSync(rootPath)) continue;
|
|
153
|
+
visit(rootPath, rootPath);
|
|
154
|
+
if (exactMatches.length >= maxResults || partialMatches.length >= maxResults) break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const matches = exactMatches.length > 0
|
|
158
|
+
? sortMatches(exactMatches).slice(0, maxResults)
|
|
159
|
+
: sortMatches(partialMatches).slice(0, maxResults);
|
|
160
|
+
|
|
161
|
+
if (matches.length === 0) {
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
message: `ไม่พบ${normalizedType === 'dir' ? 'โฟลเดอร์' : normalizedType === 'file' ? 'ไฟล์' : 'ไฟล์หรือโฟลเดอร์'}ที่ตรงกับ "${target}" ค่ะ`,
|
|
165
|
+
matches: []
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
matches: matches.map(({ path: matchPath, type }) => ({ path: matchPath, type }))
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
56
175
|
/**
|
|
57
176
|
* สร้างโฟลเดอร์ใหม่
|
|
58
177
|
* target: ชื่อโฟลเดอร์ หรือ absolute path
|
|
@@ -97,12 +216,23 @@ async function openFile(target) {
|
|
|
97
216
|
console.error('openFile error:', result);
|
|
98
217
|
return `เกิดข้อผิดพลาดในการเปิดไฟล์: ${result}`;
|
|
99
218
|
}
|
|
219
|
+
return true;
|
|
100
220
|
} else {
|
|
101
221
|
return new Promise((resolve) => {
|
|
102
|
-
|
|
222
|
+
// บน Linux ลอง xdg-open แล้วค่อย gio open ถ้าอันแรกไม่ทำงาน
|
|
223
|
+
const { exec } = require('child_process');
|
|
224
|
+
const platformCmd = process.platform === 'darwin' ? 'open' : (process.platform === 'win32' ? 'start' : 'xdg-open');
|
|
225
|
+
|
|
226
|
+
// ใช้ exec เพื่อให้รันผ่าน shell และรองรับการทำ fallback
|
|
227
|
+
let cmd = `${platformCmd} "${resolvedPath}"`;
|
|
228
|
+
if (process.platform === 'linux') {
|
|
229
|
+
cmd = `xdg-open "${resolvedPath}" || gio open "${resolvedPath}" || nautilus "${resolvedPath}"`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
exec(cmd, (err) => {
|
|
103
233
|
if (err) {
|
|
104
|
-
console.error("Failed to open path
|
|
105
|
-
resolve(
|
|
234
|
+
console.error("Failed to open path:", err);
|
|
235
|
+
resolve(`ไม่สามารถเปิดได้ค่ะ: ${err.message}`);
|
|
106
236
|
} else {
|
|
107
237
|
resolve(true);
|
|
108
238
|
}
|
|
@@ -129,7 +259,7 @@ async function deleteFile(target) {
|
|
|
129
259
|
}
|
|
130
260
|
} else {
|
|
131
261
|
return new Promise((resolve) => {
|
|
132
|
-
|
|
262
|
+
execFile('gio', ['trash', resolvedPath], (err) => {
|
|
133
263
|
if (err) {
|
|
134
264
|
console.error("Failed to trash item via gio trash:", err);
|
|
135
265
|
resolve({ success: false, message: err.message });
|
|
@@ -141,4 +271,4 @@ async function deleteFile(target) {
|
|
|
141
271
|
}
|
|
142
272
|
}
|
|
143
273
|
|
|
144
|
-
module.exports = { createFolder, openFile, deleteFile };
|
|
274
|
+
module.exports = { createFolder, openFile, deleteFile, findPath };
|
|
@@ -1,56 +1,85 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
2
|
|
|
3
|
-
function
|
|
3
|
+
function execFilePromise(command, args) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
execFile(command, args, (error) => {
|
|
6
|
+
if (error) {
|
|
7
|
+
reject(error);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
resolve();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function tryCommands(commands) {
|
|
16
|
+
let lastError = null;
|
|
17
|
+
for (const { command, args } of commands) {
|
|
18
|
+
try {
|
|
19
|
+
await execFilePromise(command, args);
|
|
20
|
+
return true;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
lastError = error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (lastError) {
|
|
27
|
+
console.error(`exec error: ${lastError}`);
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function openApp(target) {
|
|
4
33
|
if (!target) return;
|
|
5
34
|
|
|
6
|
-
let cmd = '';
|
|
7
35
|
if (process.platform === 'win32') {
|
|
8
|
-
cmd
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} else {
|
|
16
|
-
const tLower = target.toLowerCase();
|
|
17
|
-
const tCapitalized = target.charAt(0).toUpperCase() + target.slice(1).toLowerCase();
|
|
18
|
-
|
|
19
|
-
// Try common linux patterns: gtk-launch, exact name, lowercase, flatpak
|
|
36
|
+
await execFilePromise('cmd.exe', ['/c', 'start', '', target]).catch((error) => {
|
|
37
|
+
console.error(`exec error: ${error}`);
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (process.platform === 'darwin') {
|
|
20
43
|
if (!target.includes('/')) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`gtk-launch com.${tLower}app.${tCapitalized}`,
|
|
26
|
-
`gtk-launch com.${tLower}.${tCapitalized}`,
|
|
27
|
-
target,
|
|
28
|
-
tLower,
|
|
29
|
-
`flatpak run ${target}`, // In case target is already ID
|
|
30
|
-
`flatpak run com.${tLower}app.${tCapitalized}`,
|
|
31
|
-
`flatpak run com.${tLower}.${tCapitalized}`,
|
|
32
|
-
`flatpak run com.${tLower}.Browser`,
|
|
33
|
-
`flatpak run com.${tLower}.${target}`,
|
|
34
|
-
`flatpak run com.valvesoftware.Steam`,
|
|
35
|
-
`flatpak run net.lutris.Lutris`,
|
|
36
|
-
`snap run ${tLower}`
|
|
37
|
-
];
|
|
38
|
-
cmd = patterns.join(' || ');
|
|
44
|
+
await tryCommands([
|
|
45
|
+
{ command: 'open', args: ['-X', '-a', target] },
|
|
46
|
+
{ command: 'open', args: ['-a', target] }
|
|
47
|
+
]);
|
|
39
48
|
} else {
|
|
40
|
-
|
|
49
|
+
await execFilePromise('open', [target]).catch((error) => {
|
|
50
|
+
console.error(`exec error: ${error}`);
|
|
51
|
+
});
|
|
41
52
|
}
|
|
53
|
+
return;
|
|
42
54
|
}
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
const tLower = target.toLowerCase();
|
|
57
|
+
const tCapitalized = target.charAt(0).toUpperCase() + target.slice(1).toLowerCase();
|
|
58
|
+
|
|
59
|
+
if (target.includes('/')) {
|
|
60
|
+
await execFilePromise('xdg-open', [target]).catch((error) => {
|
|
46
61
|
console.error(`exec error: ${error}`);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await tryCommands([
|
|
67
|
+
{ command: 'gtk-launch', args: [target] },
|
|
68
|
+
{ command: 'gtk-launch', args: [tLower] },
|
|
69
|
+
{ command: 'gtk-launch', args: [tCapitalized] },
|
|
70
|
+
{ command: 'gtk-launch', args: [`com.${tLower}app.${tCapitalized}`] },
|
|
71
|
+
{ command: 'gtk-launch', args: [`com.${tLower}.${tCapitalized}`] },
|
|
72
|
+
{ command: target, args: [] },
|
|
73
|
+
{ command: tLower, args: [] },
|
|
74
|
+
{ command: 'flatpak', args: ['run', target] },
|
|
75
|
+
{ command: 'flatpak', args: ['run', `com.${tLower}app.${tCapitalized}`] },
|
|
76
|
+
{ command: 'flatpak', args: ['run', `com.${tLower}.${tCapitalized}`] },
|
|
77
|
+
{ command: 'flatpak', args: ['run', `com.${tLower}.Browser`] },
|
|
78
|
+
{ command: 'flatpak', args: ['run', `com.${tLower}.${target}`] },
|
|
79
|
+
{ command: 'flatpak', args: ['run', 'com.valvesoftware.Steam'] },
|
|
80
|
+
{ command: 'flatpak', args: ['run', 'net.lutris.Lutris'] },
|
|
81
|
+
{ command: 'snap', args: ['run', tLower] }
|
|
82
|
+
]);
|
|
54
83
|
}
|
|
55
84
|
|
|
56
85
|
module.exports = { openApp };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
2
|
|
|
3
3
|
let shell;
|
|
4
4
|
try {
|
|
@@ -17,7 +17,7 @@ function openWebsite(targetUrl) {
|
|
|
17
17
|
shell.openExternal(url);
|
|
18
18
|
} else {
|
|
19
19
|
// Fallback for Node.js (Linux focus)
|
|
20
|
-
|
|
20
|
+
execFile('xdg-open', [url], (err) => {
|
|
21
21
|
if (err) console.error("Failed to open URL via xdg_open:", err);
|
|
22
22
|
});
|
|
23
23
|
}
|
|
@@ -29,7 +29,7 @@ function openSearch(query) {
|
|
|
29
29
|
if (shell) {
|
|
30
30
|
shell.openExternal(url);
|
|
31
31
|
} else {
|
|
32
|
-
|
|
32
|
+
execFile('xdg-open', [url], (err) => {
|
|
33
33
|
if (err) console.error("Failed to open search via xdg-open:", err);
|
|
34
34
|
});
|
|
35
35
|
}
|
package/src/CLI/chat_router.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { GoogleGenAI } = require('@google/genai');
|
|
4
|
-
const { executeCodeTask } = require('./code_agent');
|
|
4
|
+
const { executeCodeTask, _helpers: codeAgentHelpers } = require('./code_agent');
|
|
5
5
|
const { readConfig, getAvailableProviders } = require('../System/config_manager');
|
|
6
6
|
|
|
7
7
|
const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
|
|
@@ -9,12 +9,15 @@ const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
|
|
|
9
9
|
const CODE_KEYWORDS = [
|
|
10
10
|
'code', 'repo', 'repository', 'project', 'workspace', 'file', 'files', 'readme',
|
|
11
11
|
'package.json', 'bug', 'fix', 'refactor', 'test', 'tests', 'build', 'lint',
|
|
12
|
-
'implement', 'feature', 'cli', 'function', 'module', 'component', 'diff'
|
|
12
|
+
'implement', 'feature', 'cli', 'function', 'module', 'component', 'diff',
|
|
13
|
+
'list', 'show', 'ls', 'dir', 'directory', 'folders' // เพิ่มคำเหล่านี้ค่ะ
|
|
13
14
|
];
|
|
14
15
|
|
|
16
|
+
|
|
15
17
|
const THAI_CODE_KEYWORDS = [
|
|
16
18
|
'โค้ด', 'โปรเจค', 'โปรเจ็กต์', 'ไฟล์', 'รีโป', 'บั๊ก', 'แก้', 'ทดสอบ', 'เทสต์',
|
|
17
|
-
'รีแฟกเตอร์', 'ฟีเจอร์', 'คอมโพเนนต์', 'ฟังก์ชัน', 'อ่าน', 'สำรวจ', 'โครงสร้าง'
|
|
19
|
+
'รีแฟกเตอร์', 'ฟีเจอร์', 'คอมโพเนนต์', 'ฟังก์ชัน', 'อ่าน', 'สำรวจ', 'โครงสร้าง',
|
|
20
|
+
'ไดเรกทอรี', 'โฟลเดอร์'
|
|
18
21
|
];
|
|
19
22
|
|
|
20
23
|
const ROUTER_PROMPT = `You classify whether a chat message should be routed to a coding agent for the current local workspace.
|
|
@@ -26,7 +29,19 @@ Return JSON only:
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
Choose "code" when the user is asking to inspect, edit, review, debug, explain, refactor, verify, or otherwise operate on the current project/workspace/codebase/files.
|
|
29
|
-
Choose "chat" for general conversation, factual Q&A,
|
|
32
|
+
Choose "chat" for general conversation, factual Q&A, small/simple requests, non-code assistant tasks, or direct file-system actions like finding/opening a folder or file by name.
|
|
33
|
+
Only choose "code" for substantial coding work that likely needs multiple steps, workspace inspection, edits, verification, or project-wide reasoning.`;
|
|
34
|
+
|
|
35
|
+
function isDirectFilesystemActionRequest(text) {
|
|
36
|
+
const input = (text || '').trim().toLowerCase();
|
|
37
|
+
if (!input) return false;
|
|
38
|
+
|
|
39
|
+
const filesystemActionPattern = /(open|find|locate|search for|look for|หา|ค้นหา|เปิด)/;
|
|
40
|
+
const filesystemTargetPattern = /(folder|directory|dir|file|โฟลเดอร์|ไฟล์|ไดเรกทอรี)/;
|
|
41
|
+
const codeOperationPattern = /(inspect|review|refactor|debug|implement|edit|change|fix|explain|analyze|สำรวจ|รีวิว|รีแฟกเตอร์|แก้|อธิบาย|วิเคราะห์)/;
|
|
42
|
+
|
|
43
|
+
return filesystemActionPattern.test(input) && filesystemTargetPattern.test(input) && !codeOperationPattern.test(input);
|
|
44
|
+
}
|
|
30
45
|
|
|
31
46
|
function workspaceLooksLikeCodebase(workspaceRoot) {
|
|
32
47
|
const markers = [
|
|
@@ -45,13 +60,31 @@ function detectCodeIntentHeuristic(text, workspaceRoot = process.cwd()) {
|
|
|
45
60
|
const input = (text || '').trim().toLowerCase();
|
|
46
61
|
if (!input) return false;
|
|
47
62
|
if (input.startsWith('/code ')) return true;
|
|
63
|
+
if (isDirectFilesystemActionRequest(input)) return false;
|
|
64
|
+
|
|
65
|
+
return isLargeCodeTaskRequest(input, workspaceRoot);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isLargeCodeTaskRequest(text, workspaceRoot = process.cwd()) {
|
|
69
|
+
const input = (text || '').trim().toLowerCase();
|
|
70
|
+
if (!input) return false;
|
|
71
|
+
if (!workspaceLooksLikeCodebase(workspaceRoot)) return false;
|
|
48
72
|
|
|
49
73
|
const hasCodeKeyword = CODE_KEYWORDS.some(keyword => input.includes(keyword));
|
|
50
74
|
const hasThaiCodeKeyword = THAI_CODE_KEYWORDS.some(keyword => input.includes(keyword));
|
|
51
|
-
const referencesProject = /โปรเจคนี้|โปรเจ็กต์นี้|this project|this repo|this repository|codebase|workspace
|
|
52
|
-
const asksForAction =
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
const referencesProject = /โปรเจคนี้|โปรเจ็กต์นี้|this project|this repo|this repository|codebase|workspace|โฟลเดอร์นี้|ในนี้/.test(input);
|
|
76
|
+
const asksForAction = /สำรวจ|ดู|แก้|เพิ่ม|ลบ|ปรับ|ตรวจ|วิเคราะห์|ลิสต์|โชว์|แสดง|มี|implement|inspect|explore|fix|update|change|refactor|review|explain|debug|list|show/.test(input);
|
|
77
|
+
const strongTaskSignal = /failing tests?|run tests?|verify|verification|bug|issue|error|refactor|implement|feature|patch|edit|modify|analyze the project|แก้บั๊ก|รันเทสต์|ทดสอบ|ตรวจสอบ|ยืนยันผล|รีแฟกเตอร์|เพิ่มฟีเจอร์|แก้โค้ด|วิเคราะห์โปรเจค/.test(input);
|
|
78
|
+
const multiStepSignal = /and|then|พร้อม|แล้ว|จากนั้น|ทั้ง|ทั่วทั้ง|ทั้งโปรเจค|project-wide|entire project|whole project/.test(input);
|
|
79
|
+
|
|
80
|
+
// If they ask for files/folder content specifically, it's a code task because Chat can't do it accurately
|
|
81
|
+
const isListFilesRequest = (hasCodeKeyword || hasThaiCodeKeyword) && /มี|โชว์|แสดง|ลิสต์|list|show|what|anything|อะไรบ้าง/.test(input) && /ไฟล์|file|folder|dir|โฟลเดอร์/.test(input);
|
|
82
|
+
|
|
83
|
+
if (isListFilesRequest) return true;
|
|
84
|
+
if (referencesProject && strongTaskSignal) return true;
|
|
85
|
+
if ((hasCodeKeyword || hasThaiCodeKeyword) && asksForAction && strongTaskSignal) return true;
|
|
86
|
+
if ((hasCodeKeyword || hasThaiCodeKeyword) && multiStepSignal && asksForAction) return true;
|
|
87
|
+
return false;
|
|
55
88
|
}
|
|
56
89
|
|
|
57
90
|
function getRouterClient() {
|
|
@@ -71,7 +104,7 @@ function summarizeWorkspace(workspaceRoot) {
|
|
|
71
104
|
.join(', ') || '(no obvious code markers)';
|
|
72
105
|
}
|
|
73
106
|
|
|
74
|
-
async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
|
|
107
|
+
async function detectCodeIntent(text, workspaceRoot = process.cwd(), history = []) {
|
|
75
108
|
const input = (text || '').trim();
|
|
76
109
|
if (!input) {
|
|
77
110
|
return { route: 'chat', reason: 'Empty input.' };
|
|
@@ -81,6 +114,10 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
|
|
|
81
114
|
return { route: 'code', reason: 'Explicit /code command.' };
|
|
82
115
|
}
|
|
83
116
|
|
|
117
|
+
if (isDirectFilesystemActionRequest(input)) {
|
|
118
|
+
return { route: 'chat', reason: 'Direct file-system action request.' };
|
|
119
|
+
}
|
|
120
|
+
|
|
84
121
|
const heuristicRoute = detectCodeIntentHeuristic(input, workspaceRoot);
|
|
85
122
|
const routerClient = getRouterClient();
|
|
86
123
|
if (!routerClient) {
|
|
@@ -103,7 +140,8 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
|
|
|
103
140
|
text: [
|
|
104
141
|
`Workspace: ${workspaceRoot}`,
|
|
105
142
|
`Workspace markers: ${summarizeWorkspace(workspaceRoot)}`,
|
|
106
|
-
`
|
|
143
|
+
`Context (Last 5 turns): ${history.slice(-10).map(m => `${m.sender}: ${m.text}`).join('\n')}`,
|
|
144
|
+
`Current Message: ${input}`
|
|
107
145
|
].join('\n')
|
|
108
146
|
}]
|
|
109
147
|
}]
|
|
@@ -112,6 +150,12 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
|
|
|
112
150
|
const textOutput = typeof response.text === 'function' ? response.text() : response.text;
|
|
113
151
|
const parsed = JSON.parse(textOutput);
|
|
114
152
|
const route = parsed.route === 'code' ? 'code' : 'chat';
|
|
153
|
+
if (route === 'code' && !isLargeCodeTaskRequest(input, workspaceRoot)) {
|
|
154
|
+
return {
|
|
155
|
+
route: 'chat',
|
|
156
|
+
reason: 'Request looks small enough for normal chat.'
|
|
157
|
+
};
|
|
158
|
+
}
|
|
115
159
|
return {
|
|
116
160
|
route,
|
|
117
161
|
reason: parsed.reason || (route === 'code' ? 'Model classified as code.' : 'Model classified as chat.')
|
|
@@ -126,21 +170,11 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
|
|
|
126
170
|
|
|
127
171
|
async function runChatRoutedTask(input, context) {
|
|
128
172
|
const text = input.startsWith('/code ') ? input.slice('/code '.length).trim() : input;
|
|
129
|
-
const { appendMessage, setThinking, requestApproval, setMode } = context;
|
|
173
|
+
const { appendMessage, setThinking, requestApproval, setMode, history } = context;
|
|
130
174
|
|
|
131
175
|
const config = readConfig();
|
|
132
176
|
const availableProviders = getAvailableProviders(config);
|
|
133
|
-
|
|
134
|
-
// Smart Routing Priority for Code Tasks
|
|
135
|
-
let preferredProvider = config.aiProvider || 'gemini';
|
|
136
|
-
|
|
137
|
-
// If preferred isn't actually available, try best available
|
|
138
|
-
if (!availableProviders.includes(preferredProvider)) {
|
|
139
|
-
if (availableProviders.includes('anthropic')) preferredProvider = 'anthropic';
|
|
140
|
-
else if (availableProviders.includes('openai')) preferredProvider = 'openai';
|
|
141
|
-
else if (availableProviders.includes('gemini')) preferredProvider = 'gemini';
|
|
142
|
-
else preferredProvider = availableProviders[0] || 'gemini';
|
|
143
|
-
}
|
|
177
|
+
const preferredProvider = codeAgentHelpers.selectSupportedCodeProvider(config, availableProviders);
|
|
144
178
|
|
|
145
179
|
appendMessage('system', `Routing this request to Code Mode for workspace: ${process.cwd()} using [${preferredProvider}]`);
|
|
146
180
|
if (setMode) setMode('Code');
|
|
@@ -157,7 +191,14 @@ async function runChatRoutedTask(input, context) {
|
|
|
157
191
|
cwd: process.cwd(),
|
|
158
192
|
requestApproval,
|
|
159
193
|
provider: preferredProvider,
|
|
160
|
-
|
|
194
|
+
history: history,
|
|
195
|
+
onProgress: (info) => {
|
|
196
|
+
if (context.appendCodeStep) {
|
|
197
|
+
context.appendCodeStep(info);
|
|
198
|
+
} else {
|
|
199
|
+
appendMessage('system', `[Code] ${typeof info === 'string' ? info : (info.action || info.phase)}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
161
202
|
});
|
|
162
203
|
clearInterval(timer);
|
|
163
204
|
setThinking(false);
|
|
@@ -177,5 +218,10 @@ async function runChatRoutedTask(input, context) {
|
|
|
177
218
|
|
|
178
219
|
module.exports = {
|
|
179
220
|
detectCodeIntent,
|
|
180
|
-
runChatRoutedTask
|
|
221
|
+
runChatRoutedTask,
|
|
222
|
+
_helpers: {
|
|
223
|
+
detectCodeIntentHeuristic,
|
|
224
|
+
isDirectFilesystemActionRequest,
|
|
225
|
+
isLargeCodeTaskRequest
|
|
226
|
+
}
|
|
181
227
|
};
|