@pheem49/mint 1.5.2 → 1.5.3

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.
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('crypto');
3
4
  const { colors, exitWithGoodbye } = require('./cli_colors');
4
5
  const { splitResponseSentences } = require('./cli_formatters');
5
6
  const {
@@ -28,9 +29,109 @@ const systemMonitor = require('../Plugins/system_monitor');
28
29
  const workspaceManager = require('./workspace_manager');
29
30
  const { executeCodeTask } = require('./code_agent');
30
31
  const { resetChat } = require('../AI_Brain/Gemini_API');
32
+ const { saveChatImages } = require('../System/picture_store');
31
33
 
32
34
  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
33
35
 
36
+ function createSessionStats() {
37
+ return {
38
+ sessionId: crypto.randomUUID(),
39
+ startedAt: Date.now(),
40
+ activeStartedAt: null,
41
+ agentActiveMs: 0,
42
+ toolCalls: { total: 0, success: 0, failed: 0 },
43
+ modelUsage: {}
44
+ };
45
+ }
46
+
47
+ function addUsageRow(stats, row = {}) {
48
+ const provider = row.provider || 'unknown';
49
+ const model = row.model || 'unknown';
50
+ const key = `${provider}:${model}`;
51
+ if (!stats.modelUsage[key]) {
52
+ stats.modelUsage[key] = {
53
+ provider,
54
+ model,
55
+ requests: 0,
56
+ inputTokens: 0,
57
+ cacheReads: 0,
58
+ outputTokens: 0,
59
+ reasoningTokens: 0,
60
+ totalTokens: 0
61
+ };
62
+ }
63
+
64
+ const target = stats.modelUsage[key];
65
+ target.requests += Number(row.requests) || 1;
66
+ target.inputTokens += Number(row.inputTokens) || 0;
67
+ target.cacheReads += Number(row.cacheReads) || 0;
68
+ target.outputTokens += Number(row.outputTokens) || 0;
69
+ target.reasoningTokens += Number(row.reasoningTokens) || 0;
70
+ target.totalTokens += Number(row.totalTokens) || 0;
71
+ }
72
+
73
+ function normalizeProviderUsage(providerInfo = {}) {
74
+ const usage = providerInfo.usage;
75
+ if (Array.isArray(usage)) return usage;
76
+ if (!usage || typeof usage !== 'object') {
77
+ return [{
78
+ provider: providerInfo.provider,
79
+ model: providerInfo.model,
80
+ requests: 1
81
+ }];
82
+ }
83
+
84
+ return [{
85
+ provider: providerInfo.provider,
86
+ model: providerInfo.model,
87
+ requests: 1,
88
+ inputTokens: usage.promptTokenCount || usage.prompt_tokens || usage.input_tokens,
89
+ cacheReads: usage.cachedContentTokenCount ||
90
+ (usage.prompt_tokens_details && usage.prompt_tokens_details.cached_tokens) ||
91
+ usage.cache_read_input_tokens,
92
+ outputTokens: usage.candidatesTokenCount || usage.completion_tokens || usage.output_tokens,
93
+ reasoningTokens: usage.thoughtsTokenCount ||
94
+ (usage.completion_tokens_details && usage.completion_tokens_details.reasoning_tokens),
95
+ totalTokens: usage.totalTokenCount || usage.total_tokens
96
+ }];
97
+ }
98
+
99
+ function recordProviderInfo(stats, providerInfo) {
100
+ if (!providerInfo) return;
101
+ for (const row of normalizeProviderUsage(providerInfo)) {
102
+ addUsageRow(stats, row);
103
+ }
104
+ }
105
+
106
+ function markAgentActive(stats, active) {
107
+ const now = Date.now();
108
+ if (active && !stats.activeStartedAt) {
109
+ stats.activeStartedAt = now;
110
+ return;
111
+ }
112
+ if (!active && stats.activeStartedAt) {
113
+ stats.agentActiveMs += now - stats.activeStartedAt;
114
+ stats.activeStartedAt = null;
115
+ }
116
+ }
117
+
118
+ function buildExitSummary(stats) {
119
+ const activeMs = stats.agentActiveMs + (stats.activeStartedAt ? Date.now() - stats.activeStartedAt : 0);
120
+ const total = stats.toolCalls.total;
121
+ return {
122
+ message: 'Agent powering down. Goodbye!',
123
+ sessionId: stats.sessionId,
124
+ toolCalls: {
125
+ ...stats.toolCalls,
126
+ successRate: total ? (stats.toolCalls.success / total) * 100 : 0
127
+ },
128
+ wallMs: Date.now() - stats.startedAt,
129
+ agentActiveMs: activeMs,
130
+ modelUsage: Object.values(stats.modelUsage),
131
+ quotaHint: 'Use /models to view model quota information'
132
+ };
133
+ }
134
+
34
135
  // ---------------------------------------------------------------------------
35
136
  // Internal helpers
36
137
  // ---------------------------------------------------------------------------
@@ -143,12 +244,25 @@ async function sendSemanticCodeMessage({ rawArgs = '', appendMessage, streamMess
143
244
  }
144
245
  }
145
246
 
146
- async function sendImageMessage({ images, image, prompt, appendMessage, streamMessage, setThinking, appendCodeStep }) {
247
+ function hasAllImageLabels(message = '', imageCount = 0) {
248
+ const text = String(message || '');
249
+ for (let index = 1; index <= imageCount; index++) {
250
+ if (!text.includes(`[Image #${index}]`)) return false;
251
+ }
252
+ return imageCount > 0;
253
+ }
254
+
255
+ function formatImageDisplayMessage(message = '', labels = '', imageCount = 0) {
256
+ if (!labels) return message;
257
+ return hasAllImageLabels(message, imageCount) ? message : `${message}\n${labels}`;
258
+ }
259
+
260
+ async function sendImageMessage({ images, image, prompt, appendMessage, streamMessage, setThinking, appendCodeStep, stats }) {
147
261
  const formatErr = (err) => err && err.message ? err.message : String(err || 'Unknown error');
148
262
  const imageList = images || (image ? [image] : []);
149
263
  const message = prompt || 'Analyze this image.';
150
264
  const labels = imageList.map((_, i) => `[Image #${i + 1}]`).join(' ');
151
- const displayMessage = labels && message.includes(labels) ? message : `${message}\n${labels}`;
265
+ const displayMessage = formatImageDisplayMessage(message, labels, imageList.length);
152
266
 
153
267
  appendMessage('user', displayMessage);
154
268
  if (appendCodeStep) {
@@ -160,17 +274,27 @@ async function sendImageMessage({ images, image, prompt, appendMessage, streamMe
160
274
  }
161
275
 
162
276
  const cancelTimer = startThinkingTimer(setThinking);
277
+ if (stats) markAgentActive(stats, true);
163
278
  try {
164
- const result = await handleChat(message, imageList.map(item => item.dataUri), null);
279
+ const imageDataUris = imageList.map(item => item.dataUri);
280
+ const result = await handleChat(message, imageDataUris, null);
281
+ try {
282
+ saveChatImages(imageDataUris, { source: 'cli', message });
283
+ } catch (saveError) {
284
+ console.error('[Pictures] Failed to save CLI image:', saveError.message);
285
+ }
165
286
  cancelTimer();
166
287
  setThinking(false);
288
+ if (stats) markAgentActive(stats, false);
167
289
 
168
290
  const responseText = result.response || '';
291
+ if (stats) recordProviderInfo(stats, result.providerInfo);
169
292
  await streamAssistantSentences(responseText, appendMessage, { providerInfo: result.providerInfo }, streamMessage);
170
293
  return { responseText, labels, imageList };
171
294
  } catch (err) {
172
295
  cancelTimer();
173
296
  setThinking(false);
297
+ if (stats) markAgentActive(stats, false);
174
298
  appendMessage('error', formatErr(err));
175
299
  return { responseText: '', labels, imageList };
176
300
  }
@@ -189,6 +313,7 @@ async function runAgentTask(text, { appendMessage, streamMessage, setThinking, r
189
313
 
190
314
  if (setMode) setMode('Agent');
191
315
  const cancelTimer = startThinkingTimer(setThinking);
316
+ markAgentActive(sharedState.stats, true);
192
317
 
193
318
  try {
194
319
  const config = require('../System/config_manager').readConfig();
@@ -202,10 +327,19 @@ async function runAgentTask(text, { appendMessage, streamMessage, setThinking, r
202
327
  askUser,
203
328
  provider: preferredProvider,
204
329
  history: contextualHistory,
205
- onProgress: (info) => { if (appendCodeStep) appendCodeStep(info); },
330
+ onProgress: (info) => {
331
+ if (info && info.phase === 'tool_call') {
332
+ sharedState.stats.toolCalls.total += 1;
333
+ if (info.status === 'success') sharedState.stats.toolCalls.success += 1;
334
+ else sharedState.stats.toolCalls.failed += 1;
335
+ }
336
+ if (appendCodeStep) appendCodeStep(info);
337
+ },
206
338
  onFinalSummary: async (info) => {
207
339
  cancelTimer();
208
340
  setThinking(false);
341
+ markAgentActive(sharedState.stats, false);
342
+ recordProviderInfo(sharedState.stats, info.providerInfo);
209
343
  streamedFinalSummary = true;
210
344
  await streamAssistantSentences(info.summary, appendMessage, { providerInfo: info.providerInfo }, streamMessage);
211
345
  }
@@ -213,13 +347,16 @@ async function runAgentTask(text, { appendMessage, streamMessage, setThinking, r
213
347
 
214
348
  cancelTimer();
215
349
  setThinking(false);
350
+ markAgentActive(sharedState.stats, false);
216
351
  sharedState.lastResponseText = result.summary;
217
352
  if (!streamedFinalSummary) {
353
+ recordProviderInfo(sharedState.stats, result.providerInfo);
218
354
  await streamAssistantSentences(result.summary, appendMessage, { providerInfo: result.providerInfo }, streamMessage);
219
355
  }
220
356
  } catch (err) {
221
357
  cancelTimer();
222
358
  setThinking(false);
359
+ markAgentActive(sharedState.stats, false);
223
360
  appendMessage('error', formatErr(err));
224
361
  } finally {
225
362
  if (setMode) setMode('Agent');
@@ -242,11 +379,14 @@ async function startInteractiveChat(initialMessage = null, options = {}) {
242
379
  // Shared mutable state between onSubmit closures
243
380
  const sharedState = {
244
381
  lastResponseText: '',
245
- recentImageContextText: ''
382
+ recentImageContextText: '',
383
+ isBusy: false,
384
+ stats: createSessionStats()
246
385
  };
247
386
 
248
387
  // -----------------------------------------------------------------------
249
- const ui = await createChatUI({
388
+ let ui;
389
+ ui = await createChatUI({
250
390
  onPasteImage: async () => {
251
391
  try {
252
392
  const image = loadClipboardImageAsDataUri();
@@ -257,6 +397,12 @@ async function startInteractiveChat(initialMessage = null, options = {}) {
257
397
  },
258
398
 
259
399
  onSubmit: async (text, submitOptions = {}) => {
400
+ if (sharedState.isBusy) {
401
+ ui.appendMessage('system', 'Mint is still working on the previous request. Please wait for it to finish before sending another command.');
402
+ return;
403
+ }
404
+ sharedState.isBusy = true;
405
+
260
406
  const {
261
407
  appendMessage, streamMessage, setThinking, updateStatusModel,
262
408
  copyLastResponse, requestApproval, setMode, appendCodeStep,
@@ -264,168 +410,177 @@ async function startInteractiveChat(initialMessage = null, options = {}) {
264
410
  setPendingPasteText, setFastMode, toggleFastMode, getFastMode
265
411
  } = ui;
266
412
 
267
- // ── Image submission ────────────────────────────────────────────
268
- if (submitOptions.images && submitOptions.images.length > 0) {
269
- const images = submitOptions.images.map(item => item.image || item);
270
- const { responseText, labels, imageList } = await sendImageMessage({
271
- images,
272
- prompt: text.trim() || 'Analyze this image.',
273
- appendMessage, streamMessage, setThinking, appendCodeStep
274
- });
275
- sharedState.lastResponseText = responseText;
276
- if (responseText) {
277
- sharedState.recentImageContextText = [
278
- `Recent image context: the user attached ${imageList.length} image(s) labelled ${labels || '[Image #1]'}.`,
279
- 'The terminal UI displays image attachments as labels only; it does not render thumbnails inside the chat.',
280
- `Assistant response to those image(s): ${responseText}`
281
- ].join('\n');
282
- }
283
- return;
284
- }
285
-
286
- // ── Slash commands ──────────────────────────────────────────────
287
- if (text.startsWith('/')) {
288
- if (text.startsWith('/agent')) {
289
- const aArgs = text.split(' ');
290
- if (aArgs[1] === 'list') {
291
- appendMessage('system', `Available Agents: ${agentOrchestrator.listAgents().join(', ')}`);
292
- } else if (aArgs[1]) {
293
- const success = agentOrchestrator.setAgent(aArgs[1]);
294
- if (success) {
295
- const agent = agentOrchestrator.getCurrentAgent();
296
- appendMessage('system', `Switched to Agent: ${agent.icon} ${agent.name}`);
297
- updateStatusModel(agent.name);
298
- resetChat();
299
- } else {
300
- appendMessage('error', `Agent "${aArgs[1]}" not found. Try /agent list`);
301
- }
302
- } else {
303
- const agent = agentOrchestrator.getCurrentAgent();
304
- appendMessage('system', `Current Agent: ${agent.icon} ${agent.name}\nUsage: /agent <type> or /agent list`);
413
+ try {
414
+ // ── Image submission ────────────────────────────────────────
415
+ if (submitOptions.images && submitOptions.images.length > 0) {
416
+ const images = submitOptions.images.map(item => item.image || item);
417
+ const { responseText, labels, imageList } = await sendImageMessage({
418
+ images,
419
+ prompt: text.trim() || 'Analyze this image.',
420
+ appendMessage, streamMessage, setThinking, appendCodeStep,
421
+ stats: sharedState.stats
422
+ });
423
+ sharedState.lastResponseText = responseText;
424
+ if (responseText) {
425
+ sharedState.recentImageContextText = [
426
+ `Recent image context: the user attached ${imageList.length} image(s) labelled ${labels || '[Image #1]'}.`,
427
+ 'The terminal UI displays image attachments as labels only; it does not render thumbnails inside the chat.',
428
+ `Assistant response to those image(s): ${responseText}`
429
+ ].join('\n');
305
430
  }
306
431
  return;
307
432
  }
308
433
 
309
- if (text.startsWith('/stats')) {
310
- appendMessage('system', '📊 Fetching system statistics...');
311
- const stats = await systemMonitor.execute('stats');
312
- appendMessage('system', stats);
313
- return;
314
- }
315
-
316
- if (text.startsWith('/workspace')) {
317
- const wArgs = text.split(' ');
318
- const subCmd = wArgs[1];
319
- if (subCmd === 'add') {
320
- const name = wArgs[2];
321
- const wsPath = wArgs[3] || '.';
322
- const instructions = wArgs.slice(4).join(' ');
323
- if (!name) {
324
- appendMessage('error', 'Usage: /workspace add <name> [path] [instructions]');
325
- } else {
326
- workspaceManager.addWorkspace(name, wsPath, instructions);
327
- appendMessage('system', `Workspace "${name}" registered at ${require('path').resolve(wsPath)}`);
328
- resetChat();
329
- }
330
- } else if (subCmd === 'list') {
331
- const all = workspaceManager.listWorkspaces();
332
- let listMsg = 'Registered Workspaces:\n';
333
- for (const n in all) listMsg += `- ${n}: ${all[n].path}\n`;
334
- appendMessage('system', Object.keys(all).length ? listMsg : 'No workspaces registered.');
335
- } else if (subCmd === 'remove') {
336
- const name = wArgs[2];
337
- if (workspaceManager.removeWorkspace(name)) {
338
- appendMessage('system', `Removed workspace "${name}"`);
339
- resetChat();
340
- } else {
341
- appendMessage('error', `Workspace "${name}" not found.`);
342
- }
343
- } else if (subCmd === 'use' || subCmd === 'switch') {
344
- const name = wArgs[2];
345
- const all = workspaceManager.listWorkspaces();
346
- if (all[name]) {
347
- const newPath = all[name].path;
348
- try {
349
- process.chdir(newPath);
350
- updateWorkspace(newPath);
351
- appendMessage('system', `✓ Switched to workspace "${name}" at ${newPath}`);
434
+ // ── Slash commands ──────────────────────────────────────────
435
+ if (text.startsWith('/')) {
436
+ if (text.startsWith('/agent')) {
437
+ const aArgs = text.split(' ');
438
+ if (aArgs[1] === 'list') {
439
+ appendMessage('system', `Available Agents: ${agentOrchestrator.listAgents().join(', ')}`);
440
+ } else if (aArgs[1]) {
441
+ const success = agentOrchestrator.setAgent(aArgs[1]);
442
+ if (success) {
443
+ const agent = agentOrchestrator.getCurrentAgent();
444
+ appendMessage('system', `Switched to Agent: ${agent.icon} ${agent.name}`);
445
+ updateStatusModel(agent.name);
352
446
  resetChat();
353
- } catch (e) {
354
- appendMessage('error', `Failed to change directory: ${e.message}`);
447
+ } else {
448
+ appendMessage('error', `Agent "${aArgs[1]}" not found. Try /agent list`);
355
449
  }
356
450
  } else {
357
- appendMessage('error', `Workspace "${name}" not found. Try /workspace list`);
451
+ const agent = agentOrchestrator.getCurrentAgent();
452
+ appendMessage('system', `Current Agent: ${agent.icon} ${agent.name}\nUsage: /agent <type> or /agent list`);
358
453
  }
359
- } else {
360
- const ws = workspaceManager.getWorkspaceByPath(process.cwd());
361
- appendMessage('system', ws
362
- ? `Current Workspace: ${ws.name}\nPath: ${ws.path}`
363
- : `Not currently in a registered workspace.\nActive Path: ${process.cwd()}\nUsage: /workspace <add|use|list|remove>`);
454
+ return;
364
455
  }
365
- return;
366
- }
367
456
 
368
- if (text.startsWith('/review')) {
369
- if (!sharedState.lastResponseText) {
370
- appendMessage('error', 'Nothing to review yet. Get a response first.');
457
+ if (text.startsWith('/stats')) {
458
+ appendMessage('system', '📊 Fetching system statistics...');
459
+ const stats = await systemMonitor.execute('stats');
460
+ appendMessage('system', stats);
371
461
  return;
372
462
  }
373
- agentOrchestrator.setAgent('reviewer');
374
- appendMessage('system', '⚖️ Requesting second-pass review from Mint Reviewer...');
375
- text = `Please review this previous response and provide a critique:\n\n${sharedState.lastResponseText}`;
376
- } else {
377
- if (!text.startsWith('/image') && !text.startsWith('/paste')) {
378
- appendMessage('user', text);
463
+
464
+ if (text.startsWith('/workspace')) {
465
+ const wArgs = text.split(' ');
466
+ const subCmd = wArgs[1];
467
+ if (subCmd === 'add') {
468
+ const name = wArgs[2];
469
+ const wsPath = wArgs[3] || '.';
470
+ const instructions = wArgs.slice(4).join(' ');
471
+ if (!name) {
472
+ appendMessage('error', 'Usage: /workspace add <name> [path] [instructions]');
473
+ } else {
474
+ workspaceManager.addWorkspace(name, wsPath, instructions);
475
+ appendMessage('system', `Workspace "${name}" registered at ${require('path').resolve(wsPath)}`);
476
+ resetChat();
477
+ }
478
+ } else if (subCmd === 'list') {
479
+ const all = workspaceManager.listWorkspaces();
480
+ let listMsg = 'Registered Workspaces:\n';
481
+ for (const n in all) listMsg += `- ${n}: ${all[n].path}\n`;
482
+ appendMessage('system', Object.keys(all).length ? listMsg : 'No workspaces registered.');
483
+ } else if (subCmd === 'remove') {
484
+ const name = wArgs[2];
485
+ if (workspaceManager.removeWorkspace(name)) {
486
+ appendMessage('system', `Removed workspace "${name}"`);
487
+ resetChat();
488
+ } else {
489
+ appendMessage('error', `Workspace "${name}" not found.`);
490
+ }
491
+ } else if (subCmd === 'use' || subCmd === 'switch') {
492
+ const name = wArgs[2];
493
+ const all = workspaceManager.listWorkspaces();
494
+ if (all[name]) {
495
+ const newPath = all[name].path;
496
+ try {
497
+ process.chdir(newPath);
498
+ updateWorkspace(newPath);
499
+ appendMessage('system', `✓ Switched to workspace "${name}" at ${newPath}`);
500
+ resetChat();
501
+ } catch (e) {
502
+ appendMessage('error', `Failed to change directory: ${e.message}`);
503
+ }
504
+ } else {
505
+ appendMessage('error', `Workspace "${name}" not found. Try /workspace list`);
506
+ }
507
+ } else {
508
+ const ws = workspaceManager.getWorkspaceByPath(process.cwd());
509
+ appendMessage('system', ws
510
+ ? `Current Workspace: ${ws.name}\nPath: ${ws.path}`
511
+ : `Not currently in a registered workspace.\nActive Path: ${process.cwd()}\nUsage: /workspace <add|use|list|remove>`);
512
+ }
513
+ return;
379
514
  }
380
- const slashResult = await handleSlashCommandUI(
381
- text, appendMessage, updateStatusModel, copyLastResponse,
382
- setThinking, requestApproval, setMode, appendCodeStep, updateWorkspace, {
383
- sendImageMessage, formatErrorMessage: formatErr,
384
- attachImage, setInputText, setPendingPasteText,
385
- setFastMode, toggleFastMode, getFastMode,
386
- sendRepoSummaryMessage, sendSymbolIndexMessage,
387
- sendSemanticCodeMessage, streamAssistantSentences,
388
- streamMessage
515
+
516
+ if (text.startsWith('/review')) {
517
+ if (!sharedState.lastResponseText) {
518
+ appendMessage('error', 'Nothing to review yet. Get a response first.');
519
+ return;
389
520
  }
390
- );
391
- if (slashResult && slashResult.lastResponseText) {
392
- sharedState.lastResponseText = slashResult.lastResponseText;
521
+ agentOrchestrator.setAgent('reviewer');
522
+ appendMessage('system', '⚖️ Requesting second-pass review from Mint Reviewer...');
523
+ text = `Please review this previous response and provide a critique:\n\n${sharedState.lastResponseText}`;
524
+ } else {
525
+ if (!text.startsWith('/image') && !text.startsWith('/paste')) {
526
+ appendMessage('user', text);
527
+ }
528
+ const slashResult = await handleSlashCommandUI(
529
+ text, appendMessage, updateStatusModel, copyLastResponse,
530
+ setThinking, requestApproval, setMode, appendCodeStep, updateWorkspace, {
531
+ sendImageMessage: (args) => sendImageMessage({ ...args, stats: sharedState.stats }),
532
+ formatErrorMessage: formatErr,
533
+ attachImage, setInputText, setPendingPasteText,
534
+ setFastMode, toggleFastMode, getFastMode,
535
+ sendRepoSummaryMessage, sendSymbolIndexMessage,
536
+ sendSemanticCodeMessage, streamAssistantSentences,
537
+ streamMessage
538
+ }
539
+ );
540
+ if (slashResult && slashResult.lastResponseText) {
541
+ sharedState.lastResponseText = slashResult.lastResponseText;
542
+ }
543
+ return;
393
544
  }
394
- return;
395
545
  }
396
- }
397
546
 
398
- appendMessage('user', text);
547
+ appendMessage('user', text);
399
548
 
400
- // ── Local tool shortcuts (natural language) ─────────────────────
401
- if (isRepoSummaryRequest(text)) {
402
- const r = await sendRepoSummaryMessage({ appendMessage, streamMessage, setThinking });
403
- sharedState.lastResponseText = r;
404
- return;
405
- }
406
- if (isSymbolIndexRequest(text)) {
407
- const r = await sendSymbolIndexMessage({ appendMessage, streamMessage, setThinking });
408
- sharedState.lastResponseText = r;
409
- return;
410
- }
411
- if (isSemanticCodeSearchRequest(text)) {
412
- const query = extractSemanticCodeQuery(text);
413
- const r = await sendSemanticCodeMessage({
414
- rawArgs: `search ${query}`,
415
- appendMessage, streamMessage, setThinking, appendCodeStep
416
- });
417
- sharedState.lastResponseText = r;
418
- return;
419
- }
549
+ // ── Local tool shortcuts (natural language) ─────────────────
550
+ if (isRepoSummaryRequest(text)) {
551
+ const r = await sendRepoSummaryMessage({ appendMessage, streamMessage, setThinking });
552
+ sharedState.lastResponseText = r;
553
+ return;
554
+ }
555
+ if (isSymbolIndexRequest(text)) {
556
+ const r = await sendSymbolIndexMessage({ appendMessage, streamMessage, setThinking });
557
+ sharedState.lastResponseText = r;
558
+ return;
559
+ }
560
+ if (isSemanticCodeSearchRequest(text)) {
561
+ const query = extractSemanticCodeQuery(text);
562
+ const r = await sendSemanticCodeMessage({
563
+ rawArgs: `search ${query}`,
564
+ appendMessage, streamMessage, setThinking, appendCodeStep
565
+ });
566
+ sharedState.lastResponseText = r;
567
+ return;
568
+ }
420
569
 
421
- // ── Normal agent task ───────────────────────────────────────────
422
- await runAgentTask(text, {
423
- appendMessage, streamMessage, setThinking,
424
- requestApproval, askUser, setMode, appendCodeStep
425
- }, sharedState);
570
+ // ── Default to guarded Code Agent ───────────────────────────
571
+ await runAgentTask(text, {
572
+ appendMessage, streamMessage, setThinking,
573
+ requestApproval, askUser, setMode, appendCodeStep
574
+ }, sharedState);
575
+ } finally {
576
+ sharedState.isBusy = false;
577
+ }
426
578
  },
427
579
 
428
- onExit: () => exitWithGoodbye(0)
580
+ onExit: () => {
581
+ if (ui && typeof ui.unmount === 'function') ui.unmount();
582
+ exitWithGoodbye(0, buildExitSummary(sharedState.stats));
583
+ }
429
584
  });
430
585
 
431
586
  // ── Handle initial CLI --image option ───────────────────────────────────
@@ -434,7 +589,8 @@ async function startInteractiveChat(initialMessage = null, options = {}) {
434
589
  const image = loadImageAsDataUri(options.imagePath);
435
590
  const prompt = initialMessage || 'Analyze this image.';
436
591
  const { responseText, labels, imageList } = await sendImageMessage({
437
- images: [image], prompt, appendMessage, streamMessage, setThinking, appendCodeStep
592
+ images: [image], prompt, appendMessage, streamMessage, setThinking, appendCodeStep,
593
+ stats: sharedState.stats
438
594
  });
439
595
  sharedState.lastResponseText = responseText;
440
596
  if (responseText) {
@@ -476,4 +632,10 @@ async function startInteractiveChat(initialMessage = null, options = {}) {
476
632
  }
477
633
  }
478
634
 
479
- module.exports = { startInteractiveChat };
635
+ module.exports = {
636
+ startInteractiveChat,
637
+ _helpers: {
638
+ hasAllImageLabels,
639
+ formatImageDisplayMessage
640
+ }
641
+ };
@@ -169,8 +169,8 @@ async function handleSlashCommandUI(
169
169
  try {
170
170
  const image = loadImageAsDataUri(imagePath);
171
171
  if (helpers.attachImage) {
172
- helpers.attachImage({ label: image.path, image });
173
172
  if (prompt && helpers.setInputText) helpers.setInputText(prompt);
173
+ helpers.attachImage({ label: image.path, image });
174
174
  appendMessage('system', 'Attached image. Press Enter to send.');
175
175
  } else {
176
176
  appendMessage('error', 'Image attachment is not available in this UI.');
@@ -186,9 +186,9 @@ async function handleSlashCommandUI(
186
186
  try {
187
187
  const image = loadClipboardImageAsDataUri();
188
188
  if (helpers.attachImage) {
189
- helpers.attachImage({ label: image.path, image });
190
189
  const prompt = args.join(' ').trim();
191
190
  if (prompt && helpers.setInputText) helpers.setInputText(prompt);
191
+ helpers.attachImage({ label: image.path, image });
192
192
  appendMessage('system', 'Attached clipboard image. Press Enter to send.');
193
193
  } else {
194
194
  appendMessage('error', 'Image attachment is not available in this UI.');
@@ -112,6 +112,25 @@ function formatUpdateError(error) {
112
112
  return `Update failed: ${detail || 'Unknown npm error'}`;
113
113
  }
114
114
 
115
+ function formatUpdateCheckError(error) {
116
+ const detail = [error.stderr, error.stdout, error.message].filter(Boolean).join('\n').trim();
117
+ if (/E404|404 Not Found|not in this registry/i.test(detail)) {
118
+ return [
119
+ `Update check unavailable: ${pkg.name} is not published on the npm registry.`,
120
+ 'This does not mean your local Mint version is outdated.'
121
+ ].join('\n');
122
+ }
123
+
124
+ if (/ENOTFOUND|ETIMEDOUT|ECONNRESET|ECONNREFUSED|network|timeout/i.test(detail)) {
125
+ return [
126
+ 'Update check unavailable: npm registry could not be reached.',
127
+ 'This does not mean your local Mint version is outdated.'
128
+ ].join('\n');
129
+ }
130
+
131
+ return `Update check unavailable: ${detail || 'Unknown npm error'}`;
132
+ }
133
+
115
134
  async function runUpdate(options = {}) {
116
135
  const currentVersion = pkg.version;
117
136
  let latestVersion = '';
@@ -123,7 +142,7 @@ async function runUpdate(options = {}) {
123
142
  status: 'error',
124
143
  currentVersion,
125
144
  latestVersion,
126
- message: formatUpdateError(error)
145
+ message: formatUpdateCheckError(error)
127
146
  };
128
147
  }
129
148
 
@@ -205,6 +224,7 @@ module.exports = {
205
224
  _private: {
206
225
  parseVersion,
207
226
  formatUpdateError,
227
+ formatUpdateCheckError,
208
228
  getAutoUpdateIntervalMs
209
229
  }
210
230
  };