@pheem49/mint 1.5.1 → 1.5.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.
Files changed (33) hide show
  1. package/README.md +8 -0
  2. package/mint-cli.js +148 -921
  3. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
  4. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
  5. package/package.json +18 -20
  6. package/src/AI_Brain/proactive_engine.js +12 -2
  7. package/src/Automation_Layer/browser_automation.js +26 -24
  8. package/src/CLI/approval_handler.js +42 -0
  9. package/src/CLI/chat_ui.js +192 -7
  10. package/src/CLI/cli_colors.js +32 -0
  11. package/src/CLI/cli_formatters.js +89 -0
  12. package/src/CLI/code_agent.js +166 -57
  13. package/src/CLI/intent_detectors.js +181 -0
  14. package/src/CLI/interactive_chat.js +479 -0
  15. package/src/CLI/list_features.js +3 -0
  16. package/src/CLI/repo_summarizer.js +282 -0
  17. package/src/CLI/semantic_code_search.js +312 -0
  18. package/src/CLI/skill_manager.js +41 -0
  19. package/src/CLI/slash_command_handler.js +418 -0
  20. package/src/CLI/symbol_indexer.js +231 -0
  21. package/src/Channels/discord_bridge.js +11 -13
  22. package/src/Channels/line_bridge.js +10 -10
  23. package/src/Channels/slack_bridge.js +7 -12
  24. package/src/Channels/telegram_bridge.js +6 -14
  25. package/src/Channels/whatsapp_bridge.js +11 -9
  26. package/src/System/chat_history_manager.js +20 -12
  27. package/src/System/optional_require.js +23 -0
  28. package/src/UI/live2d_manager.js +211 -13
  29. package/src/UI/renderer.js +163 -3
  30. package/src/UI/settings.css +655 -420
  31. package/src/UI/settings.html +478 -432
  32. package/src/UI/settings.js +10 -8
  33. package/src/UI/styles.css +89 -25
@@ -5,6 +5,36 @@
5
5
  "Id": "Param93",
6
6
  "Value": 1.0,
7
7
  "Blend": "Add"
8
+ },
9
+ {
10
+ "Id": "Param91",
11
+ "Value": 0.45,
12
+ "Blend": "Add"
13
+ },
14
+ {
15
+ "Id": "ParamAngleY",
16
+ "Value": -8.0,
17
+ "Blend": "Add"
18
+ },
19
+ {
20
+ "Id": "ParamAngleZ",
21
+ "Value": 8.0,
22
+ "Blend": "Add"
23
+ },
24
+ {
25
+ "Id": "ParamEyeBallY",
26
+ "Value": -0.35,
27
+ "Blend": "Add"
28
+ },
29
+ {
30
+ "Id": "ParamMouthForm",
31
+ "Value": 0.25,
32
+ "Blend": "Add"
33
+ },
34
+ {
35
+ "Id": "ParamMouthOpenY",
36
+ "Value": 0.25,
37
+ "Blend": "Add"
8
38
  }
9
39
  ]
10
- }
40
+ }
@@ -5,6 +5,11 @@
5
5
  "Id": "Param76",
6
6
  "Value": 1.0,
7
7
  "Blend": "Add"
8
+ },
9
+ {
10
+ "Id": "Param91",
11
+ "Value": 0.75,
12
+ "Blend": "Add"
8
13
  }
9
14
  ]
10
- }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "A powerful Electron-based AI desktop assistant powered by Google Gemini, featuring screen vision, web automation, and proactive suggestions.",
5
5
  "main": "main.js",
6
6
  "scripts": {
@@ -36,40 +36,38 @@
36
36
  "type": "commonjs",
37
37
  "dependencies": {
38
38
  "@google/genai": "^1.44.0",
39
- "@hazart-pkg/live2d-core": "^1.0.1",
40
39
  "@inkjs/ui": "^2.0.0",
41
- "@line/bot-sdk": "^11.0.0",
42
- "@modelcontextprotocol/sdk": "^1.29.0",
43
- "@slack/bolt": "^4.7.2",
44
40
  "axios": "^1.13.6",
45
- "blessed": "^0.1.81",
46
41
  "cheerio": "^1.2.0",
47
42
  "commander": "^14.0.3",
48
- "discord.js": "^14.26.4",
49
43
  "dotenv": "^17.3.1",
50
- "express": "^5.2.1",
51
- "framer-motion": "^12.38.0",
52
44
  "ink": "^7.0.1",
53
45
  "ink-text-input": "^6.0.0",
54
- "inquirer": "^13.4.1",
55
- "lucide-react": "^1.9.0",
56
46
  "mammoth": "^1.12.0",
57
47
  "pdf-parse": "^2.4.5",
58
- "pixi-live2d-display": "^0.4.0",
59
- "pixi.js": "^6.5.10",
60
- "puppeteer": "^24.38.0",
61
- "qrcode-terminal": "^0.12.0",
62
- "react": "^19.2.5",
63
- "react-dom": "^19.2.5",
64
- "read-excel-file": "^9.0.9",
65
- "telegraf": "^4.16.3",
66
- "whatsapp-web.js": "^1.34.7"
48
+ "react": "^19.2.5"
49
+ },
50
+ "peerDependenciesOptional": {
51
+ "puppeteer": ">=22.0.0",
52
+ "whatsapp-web.js": ">=1.0.0",
53
+ "qrcode-terminal": ">=0.12.0",
54
+ "discord.js": ">=14.0.0",
55
+ "@slack/bolt": ">=4.0.0",
56
+ "telegraf": ">=4.0.0",
57
+ "@line/bot-sdk": ">=11.0.0",
58
+ "express": ">=4.0.0"
67
59
  },
68
60
  "devDependencies": {
61
+ "@hazart-pkg/live2d-core": "^1.0.1",
69
62
  "@vitejs/plugin-react": "^6.0.1",
70
63
  "electron": "^40.7.0",
71
64
  "electron-builder": "^26.8.1",
65
+ "framer-motion": "^12.38.0",
72
66
  "jest": "^30.4.0",
67
+ "lucide-react": "^1.9.0",
68
+ "pixi-live2d-display": "^0.4.0",
69
+ "pixi.js": "^6.5.10",
70
+ "react-dom": "^19.2.5",
73
71
  "vite": "^8.0.10"
74
72
  },
75
73
  "build": {
@@ -5,9 +5,14 @@ const { readConfig } = require('../System/config_manager');
5
5
  // Proactive Engine — Smart Suggestion Engine (Multi-Choice)
6
6
  // ============================================================
7
7
 
8
- const ai = new GoogleGenAI({});
9
8
  const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
10
9
  let lastLoggedModel = '';
10
+ let _ai = null;
11
+
12
+ function getAi(apiKey) {
13
+ if (!_ai) _ai = new GoogleGenAI({ apiKey });
14
+ return _ai;
15
+ }
11
16
 
12
17
  const PROACTIVE_SYSTEM_PROMPT = `You are a Smart Suggestion Engine built into a Desktop AI Agent called "Mint".
13
18
  Your job: observe the user's screen + behavior, then offer MULTIPLE relevant quick-action options — NOT just one question.
@@ -89,11 +94,15 @@ function getMinSuggestionIntervalMs() {
89
94
  */
90
95
  async function analyzeAndSuggest(base64Image, behaviorSummary) {
91
96
  try {
92
- const model = resolveGeminiModel();
97
+ const cfg = readConfig();
98
+ const apiKey = cfg.apiKey || process.env.GEMINI_API_KEY;
99
+ if (!apiKey) return null; // silently skip if no API key configured
100
+ const model = (cfg.geminiModel || '').trim() || DEFAULT_GEMINI_MODEL;
93
101
  if (model && model !== lastLoggedModel) {
94
102
  console.log(`[Gemini] Proactive Engine model: ${model}`);
95
103
  lastLoggedModel = model;
96
104
  }
105
+ const ai = getAi(apiKey);
97
106
 
98
107
  const now = Date.now();
99
108
  const minInterval = getMinSuggestionIntervalMs();
@@ -127,6 +136,7 @@ Rules: Only suggest if you see a clear opportunity. Return 2–4 relevant chips.
127
136
  contents: [{ role: 'user', parts: userMessage }]
128
137
  });
129
138
 
139
+
130
140
  let parsed;
131
141
  try {
132
142
  parsed = JSON.parse(response.text);
@@ -1,10 +1,10 @@
1
- const puppeteer = require('puppeteer');
1
+ 'use strict';
2
+
3
+ const { requireOptional } = require('../System/optional_require');
2
4
  const { GoogleGenAI } = require('@google/genai');
3
5
  const { readConfig } = require('../System/config_manager');
4
6
 
5
- const ai = new GoogleGenAI({});
6
- const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
7
- let lastLoggedModel = '';
7
+
8
8
 
9
9
  const BROWSER_SYSTEM_PROMPT = `You are an Autonomous Browser Agent. Your goal is to fulfill the user's web instruction by driving a headless browser.
10
10
 
@@ -24,6 +24,9 @@ Actions:
24
24
 
25
25
  You will receive the result of your previous action in the next message. If you get stuck or fail, try another approach or use "done" to report the failure.`;
26
26
 
27
+ const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
28
+ let lastLoggedModel = '';
29
+
27
30
  function resolveGeminiModel() {
28
31
  try {
29
32
  const cfg = readConfig();
@@ -35,11 +38,14 @@ function resolveGeminiModel() {
35
38
  }
36
39
 
37
40
  async function performWebAutomation(query) {
38
- if (!query) return "No query provided.";
41
+ if (!query) return 'No query provided.';
39
42
 
40
- console.log("Starting web automation for:", query);
43
+ // Dynamic require user must install puppeteer separately
44
+ const puppeteer = requireOptional('puppeteer', 'npm install puppeteer');
41
45
 
42
46
  const config = readConfig();
47
+ const apiKey = config.apiKey || process.env.GEMINI_API_KEY;
48
+ const ai = new GoogleGenAI({ apiKey });
43
49
  const browserPath = config.automationBrowser;
44
50
 
45
51
  let browser;
@@ -50,7 +56,6 @@ async function performWebAutomation(query) {
50
56
  args: ['--start-maximized']
51
57
  };
52
58
 
53
- // If it's a specific path (like /usr/bin/firefox), set executablePath
54
59
  if (browserPath && browserPath !== 'chromium') {
55
60
  launchOptions.executablePath = browserPath;
56
61
  if (browserPath.toLowerCase().includes('firefox')) {
@@ -59,9 +64,8 @@ async function performWebAutomation(query) {
59
64
  }
60
65
 
61
66
  browser = await puppeteer.launch(launchOptions);
62
-
63
67
  const page = await browser.newPage();
64
-
68
+
65
69
  const model = resolveGeminiModel();
66
70
  if (model && model !== lastLoggedModel) {
67
71
  console.log(`[Gemini] Web Automation model: ${model}`);
@@ -72,12 +76,12 @@ async function performWebAutomation(query) {
72
76
  model,
73
77
  config: {
74
78
  systemInstruction: BROWSER_SYSTEM_PROMPT,
75
- responseMimeType: "application/json"
79
+ responseMimeType: 'application/json'
76
80
  }
77
81
  });
78
82
 
79
83
  let currentObservation = `Goal: ${query}\nSystem Note: You have a blank browser page. What is your first action? Start by using "goto" to navigate to a relevant search engine or website.`;
80
-
84
+
81
85
  let maxSteps = 10;
82
86
  let step = 0;
83
87
 
@@ -87,27 +91,26 @@ async function performWebAutomation(query) {
87
91
  console.log(`Observation:`, currentObservation.substring(0, 150) + (currentObservation.length > 150 ? '...' : ''));
88
92
 
89
93
  const response = await chat.sendMessage({ message: currentObservation });
90
-
94
+
91
95
  let parsed;
92
96
  try {
93
97
  const text = response.text;
94
98
  const cleanText = text.replace(/^```json\n/, '').replace(/\n```$/, '').trim();
95
99
  parsed = JSON.parse(cleanText);
96
100
  } catch (e) {
97
- console.error("Agent failed to return valid JSON:", response.text);
98
- currentObservation = "Error: Invalid JSON returned. Please reply with ONLY valid JSON matching the schema.";
101
+ console.error('Agent failed to return valid JSON:', response.text);
102
+ currentObservation = 'Error: Invalid JSON returned. Please reply with ONLY valid JSON matching the schema.';
99
103
  continue;
100
104
  }
101
105
 
102
- console.log("Agent Thought:", parsed.thought);
103
- console.log("Agent Action:", parsed.action);
104
- console.log("Agent Target:", parsed.target);
106
+ console.log('Agent Thought:', parsed.thought);
107
+ console.log('Agent Action:', parsed.action);
108
+ console.log('Agent Target:', parsed.target);
105
109
 
106
110
  const { action, target } = parsed;
107
111
 
108
112
  if (action === 'done') {
109
- console.log("Agent finished with answer:", target);
110
- // Intentionally keeping the browser open so the user can see the page.
113
+ console.log('Agent finished with answer:', target);
111
114
  return `🤖 Web Automation Result: ${target}`;
112
115
  }
113
116
 
@@ -119,7 +122,7 @@ async function performWebAutomation(query) {
119
122
  } else if (action === 'click') {
120
123
  await page.waitForSelector(target, { timeout: 5000 });
121
124
  await page.click(target);
122
- await new Promise(r => setTimeout(r, 2000));
125
+ await new Promise(r => setTimeout(r, 2000));
123
126
  const pageTitle = await page.title();
124
127
  currentObservation = `Clicked element. Current page: ${pageTitle}. ` + await page.evaluate(() => document.body.innerText.substring(0, 1500));
125
128
  } else if (action === 'eval') {
@@ -129,16 +132,15 @@ async function performWebAutomation(query) {
129
132
  currentObservation = `Error: Unknown action type "${action}".`;
130
133
  }
131
134
  } catch (actionError) {
132
- console.error("Action execution failed:", actionError);
135
+ console.error('Action execution failed:', actionError);
133
136
  currentObservation = `Action failed: ${actionError.message}. Please try again or use another method (for instance, try a different CSS selector or just read the current page).`;
134
137
  }
135
138
  }
136
139
 
137
- // Intentionally keeping the browser open
138
- return "Agent reached maximum steps (10) without finding a final answer.";
140
+ return 'Agent reached maximum steps (10) without finding a final answer.';
139
141
 
140
142
  } catch (error) {
141
- console.error("Web Automation Error:", error);
143
+ console.error('Web Automation Error:', error);
142
144
  if (browser) browser.close();
143
145
  return `I encountered an overall error while automating the browser: ${error.message}`;
144
146
  }
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+ const { colors } = require('./cli_colors');
5
+
6
+ /**
7
+ * Prompts the user in the terminal to approve or deny a code-agent action.
8
+ * Used by the non-interactive `mint code <task>` command.
9
+ *
10
+ * @param {{ type: string, label?: string, preview?: string }} request
11
+ * @returns {Promise<boolean>} true = approved, false = denied
12
+ */
13
+ async function requestCodeApproval(request) {
14
+ const typeLabel =
15
+ request.type === 'shell' ? 'Shell Command' :
16
+ request.type === 'patch' ? 'Patch Edit' :
17
+ 'File Write';
18
+
19
+ console.log(`\n${colors.yellow}${colors.bright}[Approval Required]${colors.reset} ${typeLabel}`);
20
+ if (request.label) console.log(`${colors.gray}${request.label}${colors.reset}`);
21
+ if (request.preview) console.log(`${colors.gray}${request.preview}${colors.reset}\n`);
22
+
23
+ const rl = readline.createInterface({
24
+ input: process.stdin,
25
+ output: process.stdout
26
+ });
27
+
28
+ const answer = await new Promise((resolve) => {
29
+ rl.question('Approve this action? [y/N]: ', (value) => {
30
+ rl.close();
31
+ resolve((value || '').trim().toLowerCase());
32
+ });
33
+ });
34
+
35
+ const approved = answer === 'y' || answer === 'yes';
36
+ console.log(approved
37
+ ? `${colors.mint}[Mint Code] Approved.${colors.reset}\n`
38
+ : `${colors.pink}[Mint Code] Denied.${colors.reset}\n`);
39
+ return approved;
40
+ }
41
+
42
+ module.exports = { requestCodeApproval };
@@ -32,6 +32,127 @@ const SLASH_COMMANDS = [
32
32
  { cmd: '/exit', desc: 'Exit Mint' }
33
33
  ];
34
34
 
35
+ const MAX_BLANK_LINES = 1;
36
+
37
+ function compactPathLabel(value) {
38
+ const text = String(value || '').trim();
39
+ if (!text) return '';
40
+ return path.basename(text) || text;
41
+ }
42
+
43
+ function formatActivityStep(info = {}) {
44
+ if (!info || typeof info !== 'object') return null;
45
+
46
+ const { action, phase, target, message } = info;
47
+ const rawText = String(target || message || '').trim();
48
+ const kind = action || phase || 'activity';
49
+ if (!rawText) return null;
50
+
51
+ switch (kind) {
52
+ case 'list_files':
53
+ return { title: 'Explored', detail: `List ${rawText}` };
54
+ case 'find_path':
55
+ return { title: 'Explored', detail: `Find ${rawText}` };
56
+ case 'read_file':
57
+ return { title: 'Explored', detail: `Read ${compactPathLabel(rawText)}` };
58
+ case 'search_code':
59
+ return { title: 'Explored', detail: `Search ${rawText}` };
60
+ case 'web_search':
61
+ return { title: 'Searched', detail: rawText };
62
+ case 'warn':
63
+ return { title: '⚠ Notice', detail: rawText };
64
+ case 'run_shell':
65
+ return { title: 'Ran', detail: rawText };
66
+ case 'apply_patch':
67
+ case 'write_file':
68
+ return { title: 'Edited', detail: rawText };
69
+ case 'evaluator':
70
+ return { title: 'Checked', detail: rawText };
71
+ case 'reviewer_start':
72
+ return { title: 'Reviewing', detail: rawText };
73
+ case 'ask_user':
74
+ return { title: 'Ask User', detail: rawText };
75
+ default:
76
+ return { title: kind, detail: rawText };
77
+ }
78
+ }
79
+
80
+ function stripInlineMarkdown(value) {
81
+ return String(value || '')
82
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
83
+ .replace(/\*([^*\n]+)\*/g, '$1')
84
+ .replace(/__([^_]+)__/g, '$1')
85
+ .replace(/`([^`\n]+)`/g, '$1');
86
+ }
87
+
88
+ function cleanDisplayText(text, role = 'assistant') {
89
+ const raw = String(text || '').replace(/\r\n/g, '\n').trim();
90
+ if (!raw) return '';
91
+
92
+ const shouldPolishMarkdown = role === 'assistant' || role === 'system';
93
+ const lines = raw.split('\n');
94
+ const cleaned = [];
95
+ let inCodeBlock = false;
96
+ let blankCount = 0;
97
+
98
+ for (const sourceLine of lines) {
99
+ let line = sourceLine.replace(/\s+$/g, '');
100
+ const fence = line.match(/^\s*```(.*)$/);
101
+
102
+ if (fence) {
103
+ inCodeBlock = !inCodeBlock;
104
+ const label = fence[1] ? `code: ${fence[1].trim()}` : 'code';
105
+ line = inCodeBlock ? label : '';
106
+ } else if (inCodeBlock) {
107
+ line = line ? ` ${line}` : '';
108
+ } else if (shouldPolishMarkdown) {
109
+ const heading = line.match(/^\s{0,3}#{1,6}\s+(.+)$/);
110
+ const bullet = line.match(/^(\s*)[-*]\s+(.+)$/);
111
+ const numbered = line.match(/^(\s*)\d+[.)]\s+(.+)$/);
112
+
113
+ if (heading) {
114
+ if (cleaned.length > 0 && cleaned[cleaned.length - 1] !== '') cleaned.push('');
115
+ line = stripInlineMarkdown(heading[1]).trim();
116
+ } else if (bullet) {
117
+ line = `${bullet[1]}• ${stripInlineMarkdown(bullet[2]).trim()}`;
118
+ } else if (numbered) {
119
+ line = `${numbered[1]}${stripInlineMarkdown(line).trim()}`;
120
+ } else {
121
+ line = stripInlineMarkdown(line);
122
+ }
123
+ }
124
+
125
+ if (!line.trim()) {
126
+ blankCount++;
127
+ if (blankCount <= MAX_BLANK_LINES && cleaned.length > 0) cleaned.push('');
128
+ continue;
129
+ }
130
+
131
+ blankCount = 0;
132
+ cleaned.push(line);
133
+ }
134
+
135
+ while (cleaned[0] === '') cleaned.shift();
136
+ while (cleaned[cleaned.length - 1] === '') cleaned.pop();
137
+ return cleaned.join('\n');
138
+ }
139
+
140
+ function formatDuration(totalSeconds) {
141
+ const seconds = Math.max(0, Math.floor(Number(totalSeconds) || 0));
142
+ const minutes = Math.floor(seconds / 60);
143
+ const remainingSeconds = seconds % 60;
144
+
145
+ if (minutes <= 0) return `${remainingSeconds}s`;
146
+ return `${minutes}m ${remainingSeconds}s`;
147
+ }
148
+
149
+ function shouldAppendMessage(role, text) {
150
+ if (role === 'assistant' || role === 'system') {
151
+ return String(text || '').trim().length > 0;
152
+ }
153
+ return true;
154
+ }
155
+
35
156
  /**
36
157
  * We wrap everything in an async function to load ESM modules
37
158
  */
@@ -48,6 +169,7 @@ async function createChatUI(options) {
48
169
  const [history, setHistory] = useState(initialHistory);
49
170
  const [liveAssistant, setLiveAssistant] = useState(null);
50
171
  const [thinking, setThinking] = useState(false);
172
+ const [workingSeconds, setWorkingSeconds] = useState(0);
51
173
  const [fastMode, setFastMode] = useState(false);
52
174
  const [mode, setMode] = useState('Agent');
53
175
  const [model, setModel] = useState('');
@@ -67,6 +189,7 @@ async function createChatUI(options) {
67
189
  const pendingPasteRef = React.useRef(pendingPaste);
68
190
  const pendingPastePrefixRef = React.useRef(pendingPastePrefix);
69
191
  const liveAssistantRef = React.useRef(liveAssistant);
192
+ const thinkingStartedAtRef = React.useRef(null);
70
193
  const fastModeRef = React.useRef(fastMode);
71
194
  const suppressPasteCharRef = React.useRef(false);
72
195
  const selectedIndexRef = React.useRef(selectedIndex);
@@ -111,6 +234,17 @@ async function createChatUI(options) {
111
234
  liveAssistantRef.current = liveAssistant;
112
235
  }, [liveAssistant]);
113
236
 
237
+ useEffect(() => {
238
+ if (!thinking) return undefined;
239
+
240
+ const timer = setInterval(() => {
241
+ if (!thinkingStartedAtRef.current) return;
242
+ setWorkingSeconds(Math.floor((Date.now() - thinkingStartedAtRef.current) / 1000));
243
+ }, 1000);
244
+
245
+ return () => clearInterval(timer);
246
+ }, [thinking]);
247
+
114
248
  useEffect(() => {
115
249
  fastModeRef.current = fastMode;
116
250
  }, [fastMode]);
@@ -148,6 +282,7 @@ async function createChatUI(options) {
148
282
  // Export methods to the outside world via ref
149
283
  useImperativeHandle(ref, () => ({
150
284
  appendMessage: (role, text, metadata = {}) => {
285
+ if (!shouldAppendMessage(role, text)) return;
151
286
  setHistory(prev => [...prev, { role, text, time: new Date(), ...metadata }]);
152
287
  if (metadata.providerInfo) {
153
288
  const { provider, model } = metadata.providerInfo;
@@ -164,6 +299,7 @@ async function createChatUI(options) {
164
299
  }
165
300
  },
166
301
  appendAssistantStreamChunk: (chunk) => {
302
+ if (!String(chunk || '').trim()) return;
167
303
  const current = liveAssistantRef.current || { role: 'assistant', text: '', time: new Date() };
168
304
  const next = { ...current, text: `${current.text || ''}${chunk}` };
169
305
  liveAssistantRef.current = next;
@@ -177,7 +313,21 @@ async function createChatUI(options) {
177
313
  setHistory(prev => [...prev, current]);
178
314
  }
179
315
  },
180
- setThinking: (val) => setThinking(val),
316
+ setThinking: (val, seconds = 0) => {
317
+ if (val) {
318
+ const elapsed = Number.isFinite(seconds) ? Math.max(0, seconds) : 0;
319
+ if (!thinkingStartedAtRef.current) {
320
+ thinkingStartedAtRef.current = Date.now() - (elapsed * 1000);
321
+ }
322
+ setWorkingSeconds(Math.floor((Date.now() - thinkingStartedAtRef.current) / 1000));
323
+ setThinking(true);
324
+ return;
325
+ }
326
+
327
+ thinkingStartedAtRef.current = null;
328
+ setWorkingSeconds(0);
329
+ setThinking(false);
330
+ },
181
331
  setMode: (val) => setMode(val),
182
332
  setFastMode: (val) => {
183
333
  const next = Boolean(val);
@@ -230,6 +380,9 @@ async function createChatUI(options) {
230
380
  return;
231
381
  }
232
382
  if (thought) {
383
+ if (process.env.MINT_SHOW_THINKING_TRACE !== '1') {
384
+ return;
385
+ }
233
386
  text = thought;
234
387
  label = 'Thinking';
235
388
  labelColor = 'gray';
@@ -237,6 +390,25 @@ async function createChatUI(options) {
237
390
  } else if (action === 'thinking' || phase === 'thinking') {
238
391
  return;
239
392
  } else {
393
+ const activity = formatActivityStep(info);
394
+ if (activity) {
395
+ const fullText = `[${activity.title}] ${activity.detail}`;
396
+ if (fullText === lastSystemMessage.current) return;
397
+ lastSystemMessage.current = fullText;
398
+
399
+ setHistory(prev => [...prev, {
400
+ role: 'system',
401
+ label: activity.title,
402
+ labelColor: 'blueBright',
403
+ text: activity.detail,
404
+ isActivity: true,
405
+ activityTitle: activity.title,
406
+ activityDetail: activity.detail,
407
+ time: new Date()
408
+ }]);
409
+ return;
410
+ }
411
+
240
412
  label = action || phase || 'Action';
241
413
  text = target || message || '';
242
414
  if (!text) return;
@@ -474,6 +646,19 @@ async function createChatUI(options) {
474
646
  );
475
647
  }
476
648
 
649
+ if (msg.isActivity) {
650
+ return h(Box, { key: `${keyPrefix}-${index}`, flexDirection: 'column', marginBottom: 0 },
651
+ h(Box, null,
652
+ h(Text, { color: 'greenBright' }, '• '),
653
+ h(Text, { bold: true, color: msg.labelColor || 'blueBright' }, msg.activityTitle || msg.label || 'Activity')
654
+ ),
655
+ h(Box, { paddingLeft: 2, marginBottom: 1 },
656
+ h(Text, { color: 'gray' }, '└ '),
657
+ h(Text, { color: 'cyanBright', wrap: 'wrap' }, msg.activityDetail || msg.text)
658
+ )
659
+ );
660
+ }
661
+
477
662
  let name = 'Mint';
478
663
  let nameColor = 'greenBright';
479
664
 
@@ -494,7 +679,7 @@ async function createChatUI(options) {
494
679
  h(Text, { color: 'gray' }, ` ${msg.time instanceof Date ? msg.time.toLocaleTimeString() : ''}`)
495
680
  ),
496
681
  h(Box, { paddingLeft: 2, marginBottom: 1 },
497
- h(Text, null, msg.text)
682
+ h(Text, { wrap: 'wrap' }, cleanDisplayText(msg.text, msg.role))
498
683
  )
499
684
  );
500
685
  };
@@ -506,7 +691,8 @@ async function createChatUI(options) {
506
691
 
507
692
  // Floating (Persistent) UI part
508
693
  h(Box, { flexDirection: 'column' },
509
- thinking && h(Box, { marginBottom: 1 },
694
+ thinking && h(Box, { flexDirection: 'column', marginBottom: 1 },
695
+ h(Text, { color: 'gray', dimColor: true }, `─ Working for ${formatDuration(workingSeconds)} ─────────────────────────────────────────────────────────`),
510
696
  h(Text, { color: 'yellow' }, '● Mint is thinking...')
511
697
  ),
512
698
 
@@ -607,7 +793,7 @@ async function createChatUI(options) {
607
793
 
608
794
  return {
609
795
  appendMessage: (role, text, metadata) => ref.current?.appendMessage(role, text, metadata),
610
- setThinking: (val) => ref.current?.setThinking(val),
796
+ setThinking: (val, seconds) => ref.current?.setThinking(val, seconds),
611
797
  setMode: (val) => ref.current?.setMode(val),
612
798
  setFastMode: (val) => ref.current?.setFastMode(val),
613
799
  toggleFastMode: () => ref.current?.toggleFastMode(),
@@ -619,11 +805,10 @@ async function createChatUI(options) {
619
805
  attachImage: (image) => ref.current?.attachImage(image),
620
806
  appendCodeStep: (info) => ref.current?.appendCodeStep(info),
621
807
  streamMessage: (metadata = {}) => {
622
- let fullText = '';
623
808
  ref.current?.beginAssistantStream(metadata);
624
809
  return {
625
810
  appendChunk: (chunk) => {
626
- fullText += chunk;
811
+ if (!String(chunk || '').trim()) return;
627
812
  ref.current?.appendAssistantStreamChunk(chunk);
628
813
  },
629
814
  finalize: () => {
@@ -637,4 +822,4 @@ async function createChatUI(options) {
637
822
  };
638
823
  }
639
824
 
640
- module.exports = { createChatUI };
825
+ module.exports = { createChatUI, _helpers: { cleanDisplayText, stripInlineMarkdown, compactPathLabel, formatActivityStep, formatDuration, shouldAppendMessage } };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ANSI color constants for Mint CLI output.
5
+ */
6
+ const colors = {
7
+ reset: '\x1b[0m',
8
+ bright: '\x1b[1m',
9
+ mint: '\x1b[38;5;121m',
10
+ pink: '\x1b[38;5;213m',
11
+ gray: '\x1b[90m',
12
+ cyan: '\x1b[36m',
13
+ yellow: '\x1b[33m'
14
+ };
15
+
16
+ let isExiting = false;
17
+
18
+ /**
19
+ * Restore terminal state, print goodbye, and exit.
20
+ * @param {number} [code=0]
21
+ */
22
+ function exitWithGoodbye(code = 0) {
23
+ if (isExiting) return;
24
+ isExiting = true;
25
+
26
+ process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l');
27
+ process.stdout.write('\x1b[?25h');
28
+ console.log(`\n${colors.pink}Goodbye! See you again soon!${colors.reset}\n`);
29
+ process.exit(code);
30
+ }
31
+
32
+ module.exports = { colors, exitWithGoodbye };