@pheem49/mint 1.2.4 → 1.4.0

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.
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Mint Long-Term Memory Store
3
+ * ---------------------------
4
+ * Persists user preferences, session summaries, and usage patterns
5
+ * across all Mint sessions using SQLite (same DB as knowledge_base).
6
+ *
7
+ * Auto-injects a "User Context" block into the system prompt so Mint
8
+ * remembers who it's talking to even after restart.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+ const crypto = require('crypto');
15
+ const { readConfig } = require('../System/config_manager');
16
+
17
+ // ── Electron-safe app path ──────────────────────────────────────────────────
18
+ let app;
19
+ try {
20
+ const electron = require('electron');
21
+ app = electron.app;
22
+ } catch (_) {
23
+ app = null;
24
+ }
25
+
26
+ function getDbPath() {
27
+ const fileName = 'mint-knowledge.sqlite'; // shared DB with knowledge_base
28
+ if (app && app.getPath) {
29
+ return path.join(app.getPath('userData'), fileName);
30
+ }
31
+ const mintDir = path.join(os.homedir(), '.mint');
32
+ if (!fs.existsSync(mintDir)) fs.mkdirSync(mintDir, { recursive: true });
33
+ return path.join(mintDir, fileName);
34
+ }
35
+
36
+ // ── Lazy DatabaseSync init ─────────────────────────────────────────────────
37
+ let DatabaseSync = null;
38
+ function getDatabaseSync() {
39
+ if (!DatabaseSync) ({ DatabaseSync } = require('node:sqlite'));
40
+ return DatabaseSync;
41
+ }
42
+
43
+ let dbInstance = null;
44
+ function getDb() {
45
+ if (dbInstance) return dbInstance;
46
+ const Database = getDatabaseSync();
47
+ dbInstance = new Database(getDbPath());
48
+
49
+ dbInstance.exec(`
50
+ -- User profile: arbitrary key-value pairs
51
+ CREATE TABLE IF NOT EXISTS user_profile (
52
+ key TEXT PRIMARY KEY,
53
+ value TEXT,
54
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
55
+ );
56
+
57
+ -- Condensed summaries of past sessions
58
+ CREATE TABLE IF NOT EXISTS session_memories (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ summary TEXT NOT NULL,
61
+ tags TEXT DEFAULT '',
62
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
63
+ );
64
+
65
+ -- Frequently used topics / commands
66
+ CREATE TABLE IF NOT EXISTS usage_patterns (
67
+ pattern TEXT PRIMARY KEY,
68
+ count INTEGER DEFAULT 1,
69
+ last_used DATETIME DEFAULT CURRENT_TIMESTAMP
70
+ );
71
+
72
+ -- Response Cache: For repetitive exact queries
73
+ CREATE TABLE IF NOT EXISTS response_cache (
74
+ query_hash TEXT PRIMARY KEY,
75
+ response TEXT NOT NULL,
76
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
77
+ );
78
+ `);
79
+
80
+ return dbInstance;
81
+ }
82
+
83
+ // ── Profile helpers ────────────────────────────────────────────────────────
84
+ function setProfile(key, value) {
85
+ try {
86
+ const db = getDb();
87
+ db.prepare(`
88
+ INSERT INTO user_profile (key, value, updated_at)
89
+ VALUES (?, ?, CURRENT_TIMESTAMP)
90
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
91
+ `).run(key, String(value));
92
+ } catch (err) {
93
+ console.error('[Memory] setProfile error:', err.message);
94
+ }
95
+ }
96
+
97
+ function getProfile(key, defaultValue = null) {
98
+ try {
99
+ const row = getDb().prepare('SELECT value FROM user_profile WHERE key = ?').get(key);
100
+ return row ? row.value : defaultValue;
101
+ } catch (_) {
102
+ return defaultValue;
103
+ }
104
+ }
105
+
106
+ function getAllProfile() {
107
+ try {
108
+ const rows = getDb().prepare('SELECT key, value FROM user_profile').all();
109
+ return Object.fromEntries(rows.map(r => [r.key, r.value]));
110
+ } catch (_) {
111
+ return {};
112
+ }
113
+ }
114
+
115
+ // ── Session memory helpers ─────────────────────────────────────────────────
116
+ const MAX_SESSION_MEMORIES = 20; // keep last N summaries
117
+
118
+ function addSessionMemory(summary, tags = []) {
119
+ try {
120
+ const db = getDb();
121
+ db.prepare('INSERT INTO session_memories (summary, tags) VALUES (?, ?)').run(
122
+ summary.slice(0, 800), // cap length
123
+ tags.join(',')
124
+ );
125
+ // Prune oldest beyond limit
126
+ db.exec(`
127
+ DELETE FROM session_memories WHERE id NOT IN (
128
+ SELECT id FROM session_memories ORDER BY id DESC LIMIT ${MAX_SESSION_MEMORIES}
129
+ )
130
+ `);
131
+ } catch (err) {
132
+ console.error('[Memory] addSessionMemory error:', err.message);
133
+ }
134
+ }
135
+
136
+ function getRecentMemories(limit = 5) {
137
+ try {
138
+ return getDb()
139
+ .prepare('SELECT summary, tags, created_at FROM session_memories ORDER BY id DESC LIMIT ?')
140
+ .all(limit);
141
+ } catch (_) {
142
+ return [];
143
+ }
144
+ }
145
+
146
+ // ── Usage pattern helpers ──────────────────────────────────────────────────
147
+ function recordPattern(pattern) {
148
+ try {
149
+ const db = getDb();
150
+ db.prepare(`
151
+ INSERT INTO usage_patterns (pattern, count, last_used)
152
+ VALUES (?, 1, CURRENT_TIMESTAMP)
153
+ ON CONFLICT(pattern) DO UPDATE
154
+ SET count = count + 1, last_used = CURRENT_TIMESTAMP
155
+ `).run(pattern.slice(0, 120));
156
+ } catch (_) {}
157
+ }
158
+
159
+ function getTopPatterns(limit = 8) {
160
+ try {
161
+ return getDb()
162
+ .prepare('SELECT pattern, count FROM usage_patterns ORDER BY count DESC, last_used DESC LIMIT ?')
163
+ .all(limit);
164
+ } catch (_) {
165
+ return [];
166
+ }
167
+ }
168
+
169
+ // ── Simple keyword extractor (no external deps) ────────────────────────────
170
+ const STOP_WORDS = new Set([
171
+ 'ที่', 'ให้', 'และ', 'ของ', 'กับ', 'ใน', 'บน', 'เป็น', 'อยู่', 'มี', 'ได้', 'the', 'a', 'an',
172
+ 'is', 'are', 'was', 'were', 'it', 'in', 'on', 'at', 'for', 'to', 'of', 'with', 'and', 'or',
173
+ 'this', 'that', 'i', 'you', 'me', 'my', 'your', 'can', 'do', 'be', 'will', 'please', 'how',
174
+ 'what', 'which', 'when', 'where', 'why', 'help', 'want', 'need', 'make', 'create', 'get', 'run'
175
+ ]);
176
+
177
+ function extractKeywords(text) {
178
+ return text
179
+ .toLowerCase()
180
+ .replace(/[^\w\u0E00-\u0E7F\s]/g, ' ')
181
+ .split(/\s+/)
182
+ .filter(w => w.length > 2 && !STOP_WORDS.has(w))
183
+ .slice(0, 6);
184
+ }
185
+
186
+ // ── Main public API ────────────────────────────────────────────────────────
187
+
188
+ /**
189
+ * Called after every successful chat turn.
190
+ * Extracts patterns & infers preferences — runs async, non-blocking.
191
+ */
192
+ function recordInteraction(userMessage, aiResponseText) {
193
+ try {
194
+ if (!userMessage || !aiResponseText) return;
195
+
196
+ // Extract keywords as usage patterns
197
+ const keywords = extractKeywords(userMessage);
198
+ keywords.forEach(kw => recordPattern(kw));
199
+
200
+ // Detect preferred language
201
+ const thaiRatio = (userMessage.match(/[\u0E00-\u0E7F]/g) || []).length / userMessage.length;
202
+ if (thaiRatio > 0.3) setProfile('preferred_language', 'thai');
203
+ else setProfile('preferred_language', 'english');
204
+
205
+ // Detect coding intent (update project activity)
206
+ const codingKeywords = ['code', 'fix', 'debug', 'function', 'class', 'import', 'script',
207
+ 'แก้', 'เขียน', 'โค้ด', 'สคริปต์', 'ฟังก์ชัน'];
208
+ if (codingKeywords.some(k => userMessage.toLowerCase().includes(k))) {
209
+ const cwd = process.cwd();
210
+ if (cwd !== os.homedir()) {
211
+ setProfile('last_active_project', path.basename(cwd));
212
+ setProfile('last_active_project_path', cwd);
213
+ }
214
+ }
215
+
216
+ // Update interaction counter
217
+ const count = parseInt(getProfile('total_interactions', '0'), 10);
218
+ setProfile('total_interactions', String(count + 1));
219
+ setProfile('last_seen', new Date().toISOString());
220
+ } catch (err) {
221
+ console.error('[Memory] recordInteraction error:', err.message);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Saves a condensed summary of a completed conversation.
227
+ * Call this when user clears history or after N turns.
228
+ */
229
+ function saveSessionSummary(summary, tags = []) {
230
+ if (!summary || summary.trim().length < 10) return;
231
+ addSessionMemory(summary.trim(), tags);
232
+ }
233
+
234
+ /**
235
+ * Returns a formatted context string to inject into the AI system prompt.
236
+ * Lightweight — no async calls.
237
+ */
238
+ function getUserContext() {
239
+ try {
240
+ const profile = getAllProfile();
241
+ const patterns = getTopPatterns(6);
242
+ const memories = getRecentMemories(3);
243
+
244
+ const lines = ['\n\n[LONG-TERM USER CONTEXT — use this to personalize responses]'];
245
+
246
+ // Profile info
247
+ if (Object.keys(profile).length > 0) {
248
+ if (profile.preferred_language)
249
+ lines.push(`• Preferred language: ${profile.preferred_language}`);
250
+ if (profile.last_active_project)
251
+ lines.push(`• Last active project: ${profile.last_active_project} (${profile.last_active_project_path || ''})`);
252
+ if (profile.total_interactions)
253
+ lines.push(`• Total interactions with Mint: ${profile.total_interactions}`);
254
+ if (profile.last_seen) {
255
+ const d = new Date(profile.last_seen);
256
+ lines.push(`• Last session: ${d.toLocaleDateString('th-TH')} ${d.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit' })}`);
257
+ }
258
+ }
259
+
260
+ // Usage patterns
261
+ if (patterns.length > 0) {
262
+ const topTopics = patterns.map(p => p.pattern).join(', ');
263
+ lines.push(`• Frequent topics/tools: ${topTopics}`);
264
+ }
265
+
266
+ // Past session memories
267
+ if (memories.length > 0) {
268
+ lines.push('\nRecent session summaries:');
269
+ memories.forEach((m, i) => lines.push(` ${i + 1}. ${m.summary}`));
270
+ }
271
+
272
+ if (lines.length === 1) return ''; // nothing to add
273
+ lines.push('[END USER CONTEXT]\n');
274
+ return lines.join('\n');
275
+ } catch (err) {
276
+ console.error('[Memory] getUserContext error:', err.message);
277
+ return '';
278
+ }
279
+ }
280
+
281
+ // ── Response Cache helpers ────────────────────────────────────────────────
282
+ function getCachedResponse(query) {
283
+ try {
284
+ const hash = crypto.createHash('md5').update(query.trim().toLowerCase()).digest('hex');
285
+ const row = getDb().prepare('SELECT response, created_at FROM response_cache WHERE query_hash = ?').get(hash);
286
+ if (row) {
287
+ // Optional: check TTL (e.g., 24 hours)
288
+ const age = Date.now() - new Date(row.created_at).getTime();
289
+ if (age < 24 * 60 * 60 * 1000) {
290
+ return JSON.parse(row.response);
291
+ }
292
+ }
293
+ } catch (_) {}
294
+ return null;
295
+ }
296
+
297
+ function cacheResponse(query, responseObj) {
298
+ try {
299
+ const hash = crypto.createHash('md5').update(query.trim().toLowerCase()).digest('hex');
300
+ getDb().prepare(`
301
+ INSERT INTO response_cache (query_hash, response, created_at)
302
+ VALUES (?, ?, CURRENT_TIMESTAMP)
303
+ ON CONFLICT(query_hash) DO UPDATE SET response = excluded.response, created_at = CURRENT_TIMESTAMP
304
+ `).run(hash, JSON.stringify(responseObj));
305
+ } catch (_) {}
306
+ }
307
+
308
+ module.exports = {
309
+ recordInteraction,
310
+ saveSessionSummary,
311
+ getUserContext,
312
+ setProfile,
313
+ getProfile,
314
+ getTopPatterns,
315
+ getRecentMemories,
316
+ getCachedResponse,
317
+ cacheResponse
318
+ };
@@ -17,36 +17,42 @@ const os = require('os');
17
17
  function resolveSmartPath(target) {
18
18
  if (!target) return target;
19
19
 
20
- // 1. If it exists as is (absolute or relative to CWD), use it
21
- if (fs.existsSync(target)) return target;
22
-
20
+ const home = os.homedir();
23
21
  const commonFolders = ['Downloads', 'Desktop', 'Documents', 'Videos', 'Pictures', 'Music', 'vscode', 'Games'];
24
22
 
25
- // 2. If it starts with / and doesn't exist at root, try home directory
23
+ // 1. If it's already an absolute path and exists, use it
24
+ if (path.isAbsolute(target) && fs.existsSync(target)) return target;
25
+
26
+ // 2. If it starts with ~/ expand it
27
+ if (target.startsWith('~/')) {
28
+ const expanded = path.join(home, target.substring(2));
29
+ if (fs.existsSync(expanded)) return expanded;
30
+ }
31
+
32
+ // 3. If it starts with / but doesn't exist at root, try home directory
26
33
  if (target.startsWith('/')) {
27
- const homeRelative = path.join(os.homedir(), target.substring(1));
34
+ const homeRelative = path.join(home, target.substring(1));
28
35
  if (fs.existsSync(homeRelative)) return homeRelative;
29
-
30
- const cwdRelative = path.join(process.cwd(), target.substring(1));
31
- if (fs.existsSync(cwdRelative)) return cwdRelative;
32
-
33
- const firstPart = target.split('/')[1];
34
- if (commonFolders.includes(firstPart)) return homeRelative;
35
36
  }
36
37
 
37
- // 3. Handle ~ manually
38
- if (target.startsWith('~/')) {
39
- return path.join(os.homedir(), target.substring(2));
38
+ // 4. Check if the target itself starts with a common folder (e.g., "Downloads/resume.pdf")
39
+ const parts = target.split(/[/\\]/);
40
+ const firstPart = parts[0];
41
+ if (commonFolders.includes(firstPart)) {
42
+ const potentialPath = path.join(home, target);
43
+ if (fs.existsSync(potentialPath)) return potentialPath;
40
44
  }
41
45
 
42
- // 4. If it's just a name, search in common folders
46
+ // 5. Try searching the filename in all common folders
43
47
  for (const folder of commonFolders) {
44
- const potentialPath = path.join(os.homedir(), folder, target);
48
+ const potentialPath = path.join(home, folder, target);
45
49
  if (fs.existsSync(potentialPath)) return potentialPath;
46
50
  }
47
51
 
52
+ // 6. Final fallback: just return as is (might be relative to CWD)
48
53
  return target;
49
54
  }
55
+
50
56
  /**
51
57
  * สร้างโฟลเดอร์ใหม่
52
58
  * target: ชื่อโฟลเดอร์ หรือ absolute path
@@ -80,16 +86,32 @@ async function openFile(target) {
80
86
  if (!target) return;
81
87
  const resolvedPath = resolveSmartPath(target);
82
88
 
89
+ if (!fs.existsSync(resolvedPath)) {
90
+ console.error(`[OpenFile] File not found: ${resolvedPath}`);
91
+ return `ไม่พบไฟล์หรือโฟลเดอร์: ${target} ค่ะ`;
92
+ }
93
+
83
94
  if (shell) {
84
95
  const result = await shell.openPath(resolvedPath);
85
- if (result) console.error('openFile error:', result);
96
+ if (result) {
97
+ console.error('openFile error:', result);
98
+ return `เกิดข้อผิดพลาดในการเปิดไฟล์: ${result}`;
99
+ }
86
100
  } else {
87
- exec(`xdg-open "${resolvedPath}"`, (err) => {
88
- if (err) console.error("Failed to open path via xdg-open:", err);
101
+ return new Promise((resolve) => {
102
+ exec(`xdg-open "${resolvedPath}"`, (err) => {
103
+ if (err) {
104
+ console.error("Failed to open path via xdg-open:", err);
105
+ resolve(`ไม่สามารถเปิดไฟล์ได้ค่ะ: ${err.message}`);
106
+ } else {
107
+ resolve(true);
108
+ }
109
+ });
89
110
  });
90
111
  }
91
112
  }
92
113
 
114
+
93
115
  /**
94
116
  * ลบไฟล์หรือโฟลเดอร์ (ย้ายไป Trash)
95
117
  */
@@ -0,0 +1,181 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { GoogleGenAI } = require('@google/genai');
4
+ const { executeCodeTask } = require('./code_agent');
5
+ const { readConfig, getAvailableProviders } = require('../System/config_manager');
6
+
7
+ const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
8
+
9
+ const CODE_KEYWORDS = [
10
+ 'code', 'repo', 'repository', 'project', 'workspace', 'file', 'files', 'readme',
11
+ 'package.json', 'bug', 'fix', 'refactor', 'test', 'tests', 'build', 'lint',
12
+ 'implement', 'feature', 'cli', 'function', 'module', 'component', 'diff'
13
+ ];
14
+
15
+ const THAI_CODE_KEYWORDS = [
16
+ 'โค้ด', 'โปรเจค', 'โปรเจ็กต์', 'ไฟล์', 'รีโป', 'บั๊ก', 'แก้', 'ทดสอบ', 'เทสต์',
17
+ 'รีแฟกเตอร์', 'ฟีเจอร์', 'คอมโพเนนต์', 'ฟังก์ชัน', 'อ่าน', 'สำรวจ', 'โครงสร้าง'
18
+ ];
19
+
20
+ const ROUTER_PROMPT = `You classify whether a chat message should be routed to a coding agent for the current local workspace.
21
+
22
+ Return JSON only:
23
+ {
24
+ "route": "code" | "chat",
25
+ "reason": "short explanation"
26
+ }
27
+
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.`;
30
+
31
+ function workspaceLooksLikeCodebase(workspaceRoot) {
32
+ const markers = [
33
+ 'package.json',
34
+ '.git',
35
+ 'src',
36
+ 'README.md',
37
+ 'tsconfig.json',
38
+ 'Cargo.toml',
39
+ 'pyproject.toml'
40
+ ];
41
+ return markers.some(marker => fs.existsSync(path.join(workspaceRoot, marker)));
42
+ }
43
+
44
+ function detectCodeIntentHeuristic(text, workspaceRoot = process.cwd()) {
45
+ const input = (text || '').trim().toLowerCase();
46
+ if (!input) return false;
47
+ if (input.startsWith('/code ')) return true;
48
+
49
+ const hasCodeKeyword = CODE_KEYWORDS.some(keyword => input.includes(keyword));
50
+ const hasThaiCodeKeyword = THAI_CODE_KEYWORDS.some(keyword => input.includes(keyword));
51
+ const referencesProject = /โปรเจคนี้|โปรเจ็กต์นี้|this project|this repo|this repository|codebase|workspace/.test(input);
52
+ const asksForAction = /สำรวจ|ดู|แก้|เพิ่ม|ลบ|ปรับ|ตรวจ|วิเคราะห์|implement|inspect|explore|fix|update|change|refactor|review|explain|debug/.test(input);
53
+
54
+ return workspaceLooksLikeCodebase(workspaceRoot) && (referencesProject || ((hasCodeKeyword || hasThaiCodeKeyword) && asksForAction));
55
+ }
56
+
57
+ function getRouterClient() {
58
+ const config = readConfig();
59
+ const apiKey = (config.apiKey || process.env.GEMINI_API_KEY || '').trim();
60
+ if (!apiKey) return null;
61
+ return {
62
+ ai: new GoogleGenAI({ apiKey }),
63
+ model: (config.geminiModel || DEFAULT_GEMINI_MODEL).trim() || DEFAULT_GEMINI_MODEL
64
+ };
65
+ }
66
+
67
+ function summarizeWorkspace(workspaceRoot) {
68
+ const candidates = ['package.json', 'README.md', 'src', '.git'];
69
+ return candidates
70
+ .filter(item => fs.existsSync(path.join(workspaceRoot, item)))
71
+ .join(', ') || '(no obvious code markers)';
72
+ }
73
+
74
+ async function detectCodeIntent(text, workspaceRoot = process.cwd()) {
75
+ const input = (text || '').trim();
76
+ if (!input) {
77
+ return { route: 'chat', reason: 'Empty input.' };
78
+ }
79
+
80
+ if (input.startsWith('/code ')) {
81
+ return { route: 'code', reason: 'Explicit /code command.' };
82
+ }
83
+
84
+ const heuristicRoute = detectCodeIntentHeuristic(input, workspaceRoot);
85
+ const routerClient = getRouterClient();
86
+ if (!routerClient) {
87
+ return {
88
+ route: heuristicRoute ? 'code' : 'chat',
89
+ reason: heuristicRoute ? 'Heuristic code intent match.' : 'Heuristic chat fallback.'
90
+ };
91
+ }
92
+
93
+ try {
94
+ const response = await routerClient.ai.models.generateContent({
95
+ model: routerClient.model,
96
+ config: {
97
+ systemInstruction: ROUTER_PROMPT,
98
+ responseMimeType: 'application/json'
99
+ },
100
+ contents: [{
101
+ role: 'user',
102
+ parts: [{
103
+ text: [
104
+ `Workspace: ${workspaceRoot}`,
105
+ `Workspace markers: ${summarizeWorkspace(workspaceRoot)}`,
106
+ `Message: ${input}`
107
+ ].join('\n')
108
+ }]
109
+ }]
110
+ });
111
+
112
+ const textOutput = typeof response.text === 'function' ? response.text() : response.text;
113
+ const parsed = JSON.parse(textOutput);
114
+ const route = parsed.route === 'code' ? 'code' : 'chat';
115
+ return {
116
+ route,
117
+ reason: parsed.reason || (route === 'code' ? 'Model classified as code.' : 'Model classified as chat.')
118
+ };
119
+ } catch (error) {
120
+ return {
121
+ route: heuristicRoute ? 'code' : 'chat',
122
+ reason: heuristicRoute ? 'Heuristic fallback after router error.' : 'Chat fallback after router error.'
123
+ };
124
+ }
125
+ }
126
+
127
+ async function runChatRoutedTask(input, context) {
128
+ const text = input.startsWith('/code ') ? input.slice('/code '.length).trim() : input;
129
+ const { appendMessage, setThinking, requestApproval, setMode } = context;
130
+
131
+ const config = readConfig();
132
+ 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
+ }
144
+
145
+ appendMessage('system', `Routing this request to Code Mode for workspace: ${process.cwd()} using [${preferredProvider}]`);
146
+ if (setMode) setMode('Code');
147
+
148
+ let seconds = 0;
149
+ setThinking(true, seconds);
150
+ const timer = setInterval(() => {
151
+ seconds++;
152
+ setThinking(true, seconds);
153
+ }, 1000);
154
+
155
+ try {
156
+ const result = await executeCodeTask(text, {
157
+ cwd: process.cwd(),
158
+ requestApproval,
159
+ provider: preferredProvider,
160
+ onProgress: (message) => appendMessage('system', `[Code] ${message}`)
161
+ });
162
+ clearInterval(timer);
163
+ setThinking(false);
164
+ appendMessage('assistant', [
165
+ `Code Mode finished.`,
166
+ result.summary,
167
+ `Verification: ${result.verification}`
168
+ ].join('\n'));
169
+ } catch (error) {
170
+ clearInterval(timer);
171
+ setThinking(false);
172
+ appendMessage('error', error.message);
173
+ } finally {
174
+ if (setMode) setMode('Chat');
175
+ }
176
+ }
177
+
178
+ module.exports = {
179
+ detectCodeIntent,
180
+ runChatRoutedTask
181
+ };