@pheem49/mint 1.4.0 → 1.4.1

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.
@@ -58,11 +58,13 @@ Always respond exactly with valid JSON containing NO MARKDOWN FORMATTING (do not
58
58
  {
59
59
  "response": "Your conversational reply here (Matches user language).",
60
60
  "action": {
61
- "type": "none" | "open_url" | "open_app" | "search" | "web_automation" | "create_folder" | "open_file" | "open_folder" | "delete_file" | "clipboard_write" | "system_info" | "plugin" | "learn_file" | "learn_folder" | "system_automation" | "mcp_tool" | "mouse_click" | "mouse_move" | "type_text" | "key_tap",
61
+ "type": "none" | "open_url" | "open_app" | "search" | "web_automation" | "create_folder" | "open_file" | "open_folder" | "find_path" | "delete_file" | "clipboard_write" | "system_info" | "plugin" | "learn_file" | "learn_folder" | "system_automation" | "mcp_tool" | "mouse_click" | "mouse_move" | "type_text" | "key_tap",
62
62
 
63
63
  "pluginName": "only if type is plugin",
64
64
  "server": "only if type is mcp_tool (server name)",
65
65
  "target": "target string based on type (tool name if mcp_tool, text to type if type_text, key name if key_tap)",
66
+ "pathType": "optional for find_path: 'file' | 'dir' | 'any'",
67
+ "openAfter": true,
66
68
  "x": 0-1000, // required for mouse_click and mouse_move
67
69
  "y": 0-1000, // required for mouse_click and mouse_move
68
70
  "button": 1 | 2 | 3, // optional for mouse_click, 1=left, 2=middle, 3=right
@@ -86,6 +88,12 @@ Output: { "response": "สวัสดีค่ะ! หนูชื่อมิ
86
88
  Input: "Create a folder named Projects"
87
89
  Output: { "response": "Sure thing! I'm creating a folder named 'Projects' for you right now.", "action": { "type": "create_folder", "target": "Projects" } }
88
90
 
91
+ Input: "หาโฟลเดอร์ xidaidai ให้หน่อย" or "find the xidaidai folder"
92
+ Output: { "response": "ได้เลยค่ะ มิ้นท์จะค้นหาโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": false } }
93
+
94
+ Input: "เปิดโฟลเดอร์ xidaidai ให้หน่อย" or "open the xidaidai folder"
95
+ Output: { "response": "ได้เลยค่ะ มิ้นท์จะหาแล้วเปิดโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": true } }
96
+
89
97
  Input: "วันนี้วันที่เท่าไร" or "What date is today?" or "today's date" or "วันเวลา"
90
98
  Output: { "response": "แป๊บนึงนะคะ มิ้นท์จะดูให้ค่า", "action": { "type": "system_info", "target": "" } }
91
99
 
@@ -165,6 +173,13 @@ function resolveGeminiModel() {
165
173
  }
166
174
  }
167
175
 
176
+ function getProviderAttemptOrder(config) {
177
+ const provider = config.aiProvider || 'gemini';
178
+ const availableProviders = getAvailableProviders(config);
179
+ const alternates = availableProviders.filter(p => p !== provider);
180
+ return [provider, ...alternates];
181
+ }
182
+
168
183
  // Chat session — maintains conversation history within the session
169
184
  let chat = null;
170
185
  let activeModel = resolveGeminiModel();
@@ -214,21 +229,6 @@ function shouldUseKnowledgeSearch(message) {
214
229
  async function handleChat(message, base64Image = null, base64Audio = null) {
215
230
  try {
216
231
  const config = readConfig();
217
- const provider = config.aiProvider || 'gemini';
218
-
219
- // Ensure API Key is loaded and Client is initialized before every chat
220
- const currentKey = resolveApiKey();
221
- if (!currentKey) {
222
- return {
223
- response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
224
- action: { type: "none", target: "" }
225
- };
226
- }
227
-
228
- if (!ai || activeApiKey !== currentKey) {
229
- initAiClient();
230
- createChat(readChatHistory());
231
- }
232
232
 
233
233
  let finalMessage = message;
234
234
 
@@ -245,13 +245,7 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
245
245
  }
246
246
  }
247
247
 
248
- const { getAvailableProviders } = require('../System/config_manager');
249
- const availableProviders = getAvailableProviders(config);
250
-
251
- // Ensure the requested provider is prioritized. If not available, fallback to the first available.
252
- let providersToTry = [provider];
253
- const alternates = availableProviders.filter(p => p !== provider);
254
- providersToTry = providersToTry.concat(alternates);
248
+ const providersToTry = getProviderAttemptOrder(config);
255
249
 
256
250
  for (let i = 0; i < providersToTry.length; i++) {
257
251
  const currentProv = providersToTry[i];
@@ -272,6 +266,23 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
272
266
  return await handleHuggingFaceChat(finalMessage, base64Image, config);
273
267
  }
274
268
 
269
+ const currentKey = resolveApiKey();
270
+ if (!currentKey) {
271
+ if (i === providersToTry.length - 1) {
272
+ return {
273
+ response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
274
+ action: { type: "none", target: "" }
275
+ };
276
+ }
277
+ console.warn("[Fallback System] Gemini API key missing. Skipping Gemini provider.");
278
+ continue;
279
+ }
280
+
281
+ if (!ai || activeApiKey !== currentKey) {
282
+ initAiClient();
283
+ createChat(readChatHistory());
284
+ }
285
+
275
286
  return await handleGeminiChat(finalMessage, base64Image, base64Audio);
276
287
  } catch (error) {
277
288
  console.error(`[Fallback System] Provider '${currentProv}' failed:`, error.message);
@@ -887,5 +898,8 @@ module.exports = {
887
898
  resetChat,
888
899
  getChatTranscript,
889
900
  translateImageContent,
890
- refreshApiKeyFromConfig
901
+ refreshApiKeyFromConfig,
902
+ _helpers: {
903
+ getProviderAttemptOrder
904
+ }
891
905
  };
@@ -1,7 +1,4 @@
1
1
  const { GoogleGenAI } = require('@google/genai');
2
- const path = require('path');
3
- const fs = require('fs');
4
- const { app } = require('electron');
5
2
  const { readConfig } = require('../System/config_manager');
6
3
 
7
4
  // ============================================================
@@ -76,11 +73,8 @@ function resolveGeminiModel() {
76
73
 
77
74
  function getMinSuggestionIntervalMs() {
78
75
  try {
79
- const CONFIG_PATH = path.join(app.getPath('userData'), 'mint-config.json');
80
- if (fs.existsSync(CONFIG_PATH)) {
81
- const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
82
- return (cfg.proactiveCooldown || 120) * 1000;
83
- }
76
+ const cfg = readConfig();
77
+ return (cfg.proactiveCooldown || 120) * 1000;
84
78
  } catch {
85
79
  // ignore
86
80
  }
@@ -1,4 +1,4 @@
1
- const { exec } = require('child_process');
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
@@ -99,7 +218,7 @@ async function openFile(target) {
99
218
  }
100
219
  } else {
101
220
  return new Promise((resolve) => {
102
- exec(`xdg-open "${resolvedPath}"`, (err) => {
221
+ execFile('xdg-open', [resolvedPath], (err) => {
103
222
  if (err) {
104
223
  console.error("Failed to open path via xdg-open:", err);
105
224
  resolve(`ไม่สามารถเปิดไฟล์ได้ค่ะ: ${err.message}`);
@@ -129,7 +248,7 @@ async function deleteFile(target) {
129
248
  }
130
249
  } else {
131
250
  return new Promise((resolve) => {
132
- exec(`gio trash "${resolvedPath}"`, (err) => {
251
+ execFile('gio', ['trash', resolvedPath], (err) => {
133
252
  if (err) {
134
253
  console.error("Failed to trash item via gio trash:", err);
135
254
  resolve({ success: false, message: err.message });
@@ -141,4 +260,4 @@ async function deleteFile(target) {
141
260
  }
142
261
  }
143
262
 
144
- module.exports = { createFolder, openFile, deleteFile };
263
+ module.exports = { createFolder, openFile, deleteFile, findPath };
@@ -1,56 +1,85 @@
1
- const { exec } = require('child_process');
1
+ const { execFile } = require('child_process');
2
2
 
3
- function openApp(target) {
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 = `start "" "${target}"`;
9
- } else if (process.platform === 'darwin') {
10
- if (!target.includes('/')) {
11
- cmd = `open -X -a "${target}" || open -a "${target}"`;
12
- } else {
13
- cmd = `open "${target}"`;
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
- const patterns = [
22
- `gtk-launch ${target}`,
23
- `gtk-launch ${tLower}`,
24
- `gtk-launch ${tCapitalized}`,
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
- cmd = `xdg-open "${target}"`;
49
+ await execFilePromise('open', [target]).catch((error) => {
50
+ console.error(`exec error: ${error}`);
51
+ });
41
52
  }
53
+ return;
42
54
  }
43
55
 
44
- exec(cmd, (error) => {
45
- if (error) {
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
- if (process.platform !== 'win32') {
48
- exec(target.toLowerCase(), (err2) => {
49
- if (err2) console.error("Fallback lowercase exec failed:", err2);
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 { exec } = require('child_process');
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
- exec(`xdg-open "${url}"`, (err) => {
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
- exec(`xdg-open "${url}"`, (err) => {
32
+ execFile('xdg-open', [url], (err) => {
33
33
  if (err) console.error("Failed to open search via xdg-open:", err);
34
34
  });
35
35
  }
@@ -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';
@@ -26,7 +26,19 @@ Return JSON only:
26
26
  }
27
27
 
28
28
  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, or non-code assistant tasks.`;
29
+ 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.
30
+ Only choose "code" for substantial coding work that likely needs multiple steps, workspace inspection, edits, verification, or project-wide reasoning.`;
31
+
32
+ function isDirectFilesystemActionRequest(text) {
33
+ const input = (text || '').trim().toLowerCase();
34
+ if (!input) return false;
35
+
36
+ const filesystemActionPattern = /(open|find|locate|search for|look for|หา|ค้นหา|เปิด)/;
37
+ const filesystemTargetPattern = /(folder|directory|dir|file|โฟลเดอร์|ไฟล์|ไดเรกทอรี)/;
38
+ const codeOperationPattern = /(inspect|review|refactor|debug|implement|edit|change|fix|explain|analyze|สำรวจ|รีวิว|รีแฟกเตอร์|แก้|อธิบาย|วิเคราะห์)/;
39
+
40
+ return filesystemActionPattern.test(input) && filesystemTargetPattern.test(input) && !codeOperationPattern.test(input);
41
+ }
30
42
 
31
43
  function workspaceLooksLikeCodebase(workspaceRoot) {
32
44
  const markers = [
@@ -45,13 +57,27 @@ function detectCodeIntentHeuristic(text, workspaceRoot = process.cwd()) {
45
57
  const input = (text || '').trim().toLowerCase();
46
58
  if (!input) return false;
47
59
  if (input.startsWith('/code ')) return true;
60
+ if (isDirectFilesystemActionRequest(input)) return false;
61
+
62
+ return isLargeCodeTaskRequest(input, workspaceRoot);
63
+ }
64
+
65
+ function isLargeCodeTaskRequest(text, workspaceRoot = process.cwd()) {
66
+ const input = (text || '').trim().toLowerCase();
67
+ if (!input) return false;
68
+ if (!workspaceLooksLikeCodebase(workspaceRoot)) return false;
48
69
 
49
70
  const hasCodeKeyword = CODE_KEYWORDS.some(keyword => input.includes(keyword));
50
71
  const hasThaiCodeKeyword = THAI_CODE_KEYWORDS.some(keyword => input.includes(keyword));
51
72
  const referencesProject = /โปรเจคนี้|โปรเจ็กต์นี้|this project|this repo|this repository|codebase|workspace/.test(input);
52
73
  const asksForAction = /สำรวจ|ดู|แก้|เพิ่ม|ลบ|ปรับ|ตรวจ|วิเคราะห์|implement|inspect|explore|fix|update|change|refactor|review|explain|debug/.test(input);
74
+ const strongTaskSignal = /failing tests?|run tests?|verify|verification|bug|issue|error|refactor|implement|feature|patch|edit|modify|analyze the project|แก้บั๊ก|รันเทสต์|ทดสอบ|ตรวจสอบ|ยืนยันผล|รีแฟกเตอร์|เพิ่มฟีเจอร์|แก้โค้ด|วิเคราะห์โปรเจค/.test(input);
75
+ const multiStepSignal = /and|then|พร้อม|แล้ว|จากนั้น|ทั้ง|ทั่วทั้ง|ทั้งโปรเจค|project-wide|entire project|whole project/.test(input);
53
76
 
54
- return workspaceLooksLikeCodebase(workspaceRoot) && (referencesProject || ((hasCodeKeyword || hasThaiCodeKeyword) && asksForAction));
77
+ if (referencesProject && strongTaskSignal) return true;
78
+ if ((hasCodeKeyword || hasThaiCodeKeyword) && asksForAction && strongTaskSignal) return true;
79
+ if ((hasCodeKeyword || hasThaiCodeKeyword) && multiStepSignal && asksForAction) return true;
80
+ return false;
55
81
  }
56
82
 
57
83
  function getRouterClient() {
@@ -71,7 +97,7 @@ function summarizeWorkspace(workspaceRoot) {
71
97
  .join(', ') || '(no obvious code markers)';
72
98
  }
73
99
 
74
- async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
100
+ async function detectCodeIntent(text, workspaceRoot = process.cwd(), history = []) {
75
101
  const input = (text || '').trim();
76
102
  if (!input) {
77
103
  return { route: 'chat', reason: 'Empty input.' };
@@ -81,6 +107,10 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
81
107
  return { route: 'code', reason: 'Explicit /code command.' };
82
108
  }
83
109
 
110
+ if (isDirectFilesystemActionRequest(input)) {
111
+ return { route: 'chat', reason: 'Direct file-system action request.' };
112
+ }
113
+
84
114
  const heuristicRoute = detectCodeIntentHeuristic(input, workspaceRoot);
85
115
  const routerClient = getRouterClient();
86
116
  if (!routerClient) {
@@ -103,7 +133,8 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
103
133
  text: [
104
134
  `Workspace: ${workspaceRoot}`,
105
135
  `Workspace markers: ${summarizeWorkspace(workspaceRoot)}`,
106
- `Message: ${input}`
136
+ `Context (Last 5 turns): ${history.slice(-10).map(m => `${m.sender}: ${m.text}`).join('\n')}`,
137
+ `Current Message: ${input}`
107
138
  ].join('\n')
108
139
  }]
109
140
  }]
@@ -112,6 +143,12 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
112
143
  const textOutput = typeof response.text === 'function' ? response.text() : response.text;
113
144
  const parsed = JSON.parse(textOutput);
114
145
  const route = parsed.route === 'code' ? 'code' : 'chat';
146
+ if (route === 'code' && !isLargeCodeTaskRequest(input, workspaceRoot)) {
147
+ return {
148
+ route: 'chat',
149
+ reason: 'Request looks small enough for normal chat.'
150
+ };
151
+ }
115
152
  return {
116
153
  route,
117
154
  reason: parsed.reason || (route === 'code' ? 'Model classified as code.' : 'Model classified as chat.')
@@ -126,21 +163,11 @@ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
126
163
 
127
164
  async function runChatRoutedTask(input, context) {
128
165
  const text = input.startsWith('/code ') ? input.slice('/code '.length).trim() : input;
129
- const { appendMessage, setThinking, requestApproval, setMode } = context;
166
+ const { appendMessage, setThinking, requestApproval, setMode, history } = context;
130
167
 
131
168
  const config = readConfig();
132
169
  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
- }
170
+ const preferredProvider = codeAgentHelpers.selectSupportedCodeProvider(config, availableProviders);
144
171
 
145
172
  appendMessage('system', `Routing this request to Code Mode for workspace: ${process.cwd()} using [${preferredProvider}]`);
146
173
  if (setMode) setMode('Code');
@@ -157,6 +184,7 @@ async function runChatRoutedTask(input, context) {
157
184
  cwd: process.cwd(),
158
185
  requestApproval,
159
186
  provider: preferredProvider,
187
+ history: history,
160
188
  onProgress: (message) => appendMessage('system', `[Code] ${message}`)
161
189
  });
162
190
  clearInterval(timer);
@@ -177,5 +205,10 @@ async function runChatRoutedTask(input, context) {
177
205
 
178
206
  module.exports = {
179
207
  detectCodeIntent,
180
- runChatRoutedTask
208
+ runChatRoutedTask,
209
+ _helpers: {
210
+ detectCodeIntentHeuristic,
211
+ isDirectFilesystemActionRequest,
212
+ isLargeCodeTaskRequest
213
+ }
181
214
  };