@pheem49/mint 1.5.1 → 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.
Files changed (52) hide show
  1. package/GUIDE_TH.md +7 -7
  2. package/README.md +140 -66
  3. package/assets/Agent_Mint.png +0 -0
  4. package/assets/Settings.png +0 -0
  5. package/main.js +12 -0
  6. package/mint-cli.js +148 -921
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
  9. package/package.json +20 -21
  10. package/preload.js +2 -0
  11. package/scripts/install_linux_desktop_entry.js +48 -0
  12. package/src/AI_Brain/Gemini_API.js +194 -491
  13. package/src/AI_Brain/autonomous_brain.js +46 -19
  14. package/src/AI_Brain/headless_agent.js +21 -2
  15. package/src/AI_Brain/proactive_engine.js +12 -2
  16. package/src/AI_Brain/provider_adapter.js +358 -0
  17. package/src/Automation_Layer/browser_automation.js +26 -24
  18. package/src/CLI/approval_handler.js +47 -0
  19. package/src/CLI/chat_router.js +7 -0
  20. package/src/CLI/chat_ui.js +586 -80
  21. package/src/CLI/cli_colors.js +115 -0
  22. package/src/CLI/cli_formatters.js +94 -0
  23. package/src/CLI/code_agent.js +825 -283
  24. package/src/CLI/intent_detectors.js +181 -0
  25. package/src/CLI/interactive_chat.js +641 -0
  26. package/src/CLI/list_features.js +3 -0
  27. package/src/CLI/repo_summarizer.js +282 -0
  28. package/src/CLI/semantic_code_search.js +312 -0
  29. package/src/CLI/skill_manager.js +41 -0
  30. package/src/CLI/slash_command_handler.js +418 -0
  31. package/src/CLI/symbol_indexer.js +231 -0
  32. package/src/CLI/updater.js +21 -1
  33. package/src/Channels/discord_bridge.js +11 -13
  34. package/src/Channels/line_bridge.js +10 -10
  35. package/src/Channels/slack_bridge.js +7 -12
  36. package/src/Channels/telegram_bridge.js +6 -14
  37. package/src/Channels/whatsapp_bridge.js +11 -9
  38. package/src/System/chat_history_manager.js +20 -12
  39. package/src/System/config_manager.js +4 -1
  40. package/src/System/ipc_handlers.js +10 -0
  41. package/src/System/optional_require.js +23 -0
  42. package/src/System/picture_store.js +109 -0
  43. package/src/System/task_manager.js +127 -0
  44. package/src/System/tool_registry.js +13 -0
  45. package/src/System/window_manager.js +16 -8
  46. package/src/UI/live2d_manager.js +246 -14
  47. package/src/UI/renderer.js +620 -45
  48. package/src/UI/settings.css +738 -439
  49. package/src/UI/settings.html +487 -432
  50. package/src/UI/settings.js +44 -10
  51. package/src/UI/styles.css +1403 -106
  52. package/privacy.txt +0 -1
@@ -32,6 +32,245 @@ 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 'plan':
67
+ return { title: 'Plan', detail: rawText };
68
+ case 'apply_patch':
69
+ case 'write_file':
70
+ return { title: 'Edited', detail: rawText };
71
+ case 'evaluator':
72
+ return { title: 'Checked', detail: rawText };
73
+ case 'reviewer_start':
74
+ return { title: 'Reviewing', detail: rawText };
75
+ case 'ask_user':
76
+ return { title: 'Ask User', detail: rawText };
77
+ default:
78
+ return { title: kind, detail: rawText };
79
+ }
80
+ }
81
+
82
+ function stripInlineMarkdown(value) {
83
+ return String(value || '')
84
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
85
+ .replace(/\*([^*\n]+)\*/g, '$1')
86
+ .replace(/__([^_]+)__/g, '$1')
87
+ .replace(/`([^`\n]+)`/g, '$1');
88
+ }
89
+
90
+ function cleanDisplayText(text, role = 'assistant') {
91
+ const raw = String(text || '').replace(/\r\n/g, '\n').trim();
92
+ if (!raw) return '';
93
+
94
+ const shouldPolishMarkdown = role === 'assistant' || role === 'system';
95
+ const lines = raw.split('\n');
96
+ const cleaned = [];
97
+ let inCodeBlock = false;
98
+ let blankCount = 0;
99
+
100
+ for (const sourceLine of lines) {
101
+ let line = sourceLine.replace(/\s+$/g, '');
102
+ const fence = line.match(/^\s*```(.*)$/);
103
+
104
+ if (fence) {
105
+ inCodeBlock = !inCodeBlock;
106
+ const label = fence[1] ? `code: ${fence[1].trim()}` : 'code';
107
+ line = inCodeBlock ? label : '';
108
+ } else if (inCodeBlock) {
109
+ line = line ? ` ${line}` : '';
110
+ } else if (shouldPolishMarkdown) {
111
+ const heading = line.match(/^\s{0,3}#{1,6}\s+(.+)$/);
112
+ const bullet = line.match(/^(\s*)[-*]\s+(.+)$/);
113
+ const numbered = line.match(/^(\s*)\d+[.)]\s+(.+)$/);
114
+
115
+ if (heading) {
116
+ if (cleaned.length > 0 && cleaned[cleaned.length - 1] !== '') cleaned.push('');
117
+ line = stripInlineMarkdown(heading[1]).trim();
118
+ } else if (bullet) {
119
+ line = `${bullet[1]}• ${stripInlineMarkdown(bullet[2]).trim()}`;
120
+ } else if (numbered) {
121
+ line = `${numbered[1]}${stripInlineMarkdown(line).trim()}`;
122
+ } else {
123
+ line = stripInlineMarkdown(line);
124
+ }
125
+ }
126
+
127
+ if (!line.trim()) {
128
+ blankCount++;
129
+ if (blankCount <= MAX_BLANK_LINES && cleaned.length > 0) cleaned.push('');
130
+ continue;
131
+ }
132
+
133
+ blankCount = 0;
134
+ cleaned.push(line);
135
+ }
136
+
137
+ while (cleaned[0] === '') cleaned.shift();
138
+ while (cleaned[cleaned.length - 1] === '') cleaned.pop();
139
+ return cleaned.join('\n');
140
+ }
141
+
142
+ function formatDuration(totalSeconds) {
143
+ const seconds = Math.max(0, Math.floor(Number(totalSeconds) || 0));
144
+ const minutes = Math.floor(seconds / 60);
145
+ const remainingSeconds = seconds % 60;
146
+
147
+ if (minutes <= 0) return `${remainingSeconds}s`;
148
+ return `${minutes}m ${remainingSeconds}s`;
149
+ }
150
+
151
+ function splitDiffStatSegments(value) {
152
+ const text = String(value || '');
153
+ const match = text.match(/\(\+(\d+)\s+-(\d+)\)/);
154
+ if (!match) return [{ text, color: 'cyanBright' }];
155
+
156
+ return [
157
+ { text: text.slice(0, match.index), color: 'cyanBright' },
158
+ { text: '(', color: 'gray' },
159
+ { text: `+${match[1]}`, color: 'greenBright' },
160
+ { text: ' ', color: 'gray' },
161
+ { text: `-${match[2]}`, color: 'redBright' },
162
+ { text: ')', color: 'gray' },
163
+ { text: text.slice(match.index + match[0].length), color: 'cyanBright' }
164
+ ].filter(part => part.text);
165
+ }
166
+
167
+ const APPROVAL_CHOICES = ['approve', 'approve_session', 'deny'];
168
+ const SUGGESTION_WINDOW_SIZE = 5;
169
+
170
+ function getNextApprovalChoice(current, direction = 1) {
171
+ const choices = APPROVAL_CHOICES;
172
+ const index = choices.indexOf(current);
173
+ const start = index === -1 ? 0 : index;
174
+ return choices[(start + direction + choices.length) % choices.length];
175
+ }
176
+
177
+ function getVisibleSuggestions(suggestions, selectedIndex, limit = SUGGESTION_WINDOW_SIZE) {
178
+ const items = Array.isArray(suggestions) ? suggestions : [];
179
+ const safeLimit = Math.max(1, Number(limit) || SUGGESTION_WINDOW_SIZE);
180
+ const safeSelected = Math.min(Math.max(0, Number(selectedIndex) || 0), Math.max(0, items.length - 1));
181
+ const start = Math.min(
182
+ Math.max(0, safeSelected - safeLimit + 1),
183
+ Math.max(0, items.length - safeLimit)
184
+ );
185
+ const visible = items.slice(start, start + safeLimit);
186
+
187
+ return {
188
+ start,
189
+ visible,
190
+ current: items.length > 0 ? safeSelected + 1 : 0,
191
+ total: items.length
192
+ };
193
+ }
194
+
195
+ function parseUnifiedDiffPreview(preview) {
196
+ const lines = String(preview || '').replace(/\r\n/g, '\n').split('\n');
197
+ const files = [];
198
+ let current = null;
199
+
200
+ for (const line of lines) {
201
+ if (line.startsWith('--- a/')) {
202
+ current = {
203
+ path: line.slice('--- a/'.length),
204
+ additions: 0,
205
+ deletions: 0,
206
+ lines: []
207
+ };
208
+ files.push(current);
209
+ continue;
210
+ }
211
+
212
+ if (!current) continue;
213
+ if (line.startsWith('+++ b/')) {
214
+ current.path = line.slice('+++ b/'.length) || current.path;
215
+ continue;
216
+ }
217
+
218
+ if (line.startsWith('@@')) {
219
+ current.lines.push({ type: 'hunk', text: line });
220
+ continue;
221
+ }
222
+
223
+ if (line.startsWith('+')) {
224
+ current.additions += 1;
225
+ current.lines.push({ type: 'add', text: line });
226
+ continue;
227
+ }
228
+
229
+ if (line.startsWith('-')) {
230
+ current.deletions += 1;
231
+ current.lines.push({ type: 'delete', text: line });
232
+ continue;
233
+ }
234
+
235
+ current.lines.push({ type: 'context', text: line });
236
+ }
237
+
238
+ return files.filter(file => file.lines.length > 0 || file.additions > 0 || file.deletions > 0);
239
+ }
240
+
241
+ function isUnifiedDiffPreview(preview) {
242
+ return parseUnifiedDiffPreview(preview).length > 0;
243
+ }
244
+
245
+ function getDiffLineStyle(line = {}) {
246
+ if (line.type === 'add') return { color: 'greenBright' };
247
+ if (line.type === 'delete') return { color: 'redBright' };
248
+ if (line.type === 'hunk') return { color: 'cyanBright' };
249
+ return { color: 'gray', dimColor: true };
250
+ }
251
+
252
+ function shouldAppendMessage(role, text) {
253
+ if (role === 'assistant' || role === 'system') {
254
+ return String(text || '').trim().length > 0;
255
+ }
256
+ return true;
257
+ }
258
+
259
+ function appendInlineImageToken(value, imageIndex) {
260
+ const token = `[Image #${imageIndex}]`;
261
+ const text = String(value || '').replace(/\s*[\r\n]+\s*/g, ' ').trimEnd();
262
+ return text ? `${text} ${token}` : token;
263
+ }
264
+
265
+ function removeImageToken(value, imageIndex) {
266
+ const tokenPattern = new RegExp(`\\s*\\[Image #${imageIndex}\\]`, 'g');
267
+ return String(value || '').replace(tokenPattern, '').replace(/\s{2,}/g, ' ').trim();
268
+ }
269
+
270
+ function removeAllImageTokens(value) {
271
+ return String(value || '').replace(/\s*\[Image #\d+\]/g, '').replace(/\s{2,}/g, ' ').trim();
272
+ }
273
+
35
274
  /**
36
275
  * We wrap everything in an async function to load ESM modules
37
276
  */
@@ -48,30 +287,34 @@ async function createChatUI(options) {
48
287
  const [history, setHistory] = useState(initialHistory);
49
288
  const [liveAssistant, setLiveAssistant] = useState(null);
50
289
  const [thinking, setThinking] = useState(false);
290
+ const [workingSeconds, setWorkingSeconds] = useState(0);
51
291
  const [fastMode, setFastMode] = useState(false);
52
292
  const [mode, setMode] = useState('Agent');
53
293
  const [model, setModel] = useState('');
54
294
  const [workspace, setWorkspace] = useState(process.cwd());
55
295
  const [pendingImages, setPendingImages] = useState([]);
56
- const [pendingImagePrefix, setPendingImagePrefix] = useState('');
57
296
  const [pendingPaste, setPendingPaste] = useState(null);
58
297
  const [pendingPastePrefix, setPendingPastePrefix] = useState('');
59
298
  const [pendingApproval, setPendingApproval] = useState(null);
60
299
  const [approvalChoice, setApprovalChoice] = useState('approve');
300
+ const [approvalSessionAutoApprove, setApprovalSessionAutoApprove] = useState(false);
301
+ const [inputResetKey, setInputResetKey] = useState(0);
61
302
 
62
303
  // Suggestions State
63
304
  const [selectedIndex, setSelectedIndex] = useState(0);
64
305
  const inputRef = React.useRef(input);
65
306
  const pendingImagesRef = React.useRef(pendingImages);
66
- const pendingImagePrefixRef = React.useRef(pendingImagePrefix);
67
307
  const pendingPasteRef = React.useRef(pendingPaste);
68
308
  const pendingPastePrefixRef = React.useRef(pendingPastePrefix);
69
309
  const liveAssistantRef = React.useRef(liveAssistant);
310
+ const thinkingStartedAtRef = React.useRef(null);
70
311
  const fastModeRef = React.useRef(fastMode);
71
312
  const suppressPasteCharRef = React.useRef(false);
313
+ const suppressPasteBurstRef = React.useRef(false);
72
314
  const selectedIndexRef = React.useRef(selectedIndex);
73
315
  const pendingApprovalRef = React.useRef(null);
74
316
  const approvalChoiceRef = React.useRef('approve');
317
+ const approvalSessionAutoApproveRef = React.useRef(false);
75
318
 
76
319
  const removePasteArtifact = (value) => {
77
320
  const text = String(value || '');
@@ -86,6 +329,10 @@ async function createChatUI(options) {
86
329
  const text = String(value || '');
87
330
  return text.length > 500 || /[\r\n]/.test(text);
88
331
  };
332
+
333
+ const resetInputCursorToEnd = () => {
334
+ setInputResetKey(key => key + 1);
335
+ };
89
336
 
90
337
  useEffect(() => {
91
338
  inputRef.current = input;
@@ -95,10 +342,6 @@ async function createChatUI(options) {
95
342
  pendingImagesRef.current = pendingImages;
96
343
  }, [pendingImages]);
97
344
 
98
- useEffect(() => {
99
- pendingImagePrefixRef.current = pendingImagePrefix;
100
- }, [pendingImagePrefix]);
101
-
102
345
  useEffect(() => {
103
346
  pendingPasteRef.current = pendingPaste;
104
347
  }, [pendingPaste]);
@@ -111,6 +354,17 @@ async function createChatUI(options) {
111
354
  liveAssistantRef.current = liveAssistant;
112
355
  }, [liveAssistant]);
113
356
 
357
+ useEffect(() => {
358
+ if (!thinking) return undefined;
359
+
360
+ const timer = setInterval(() => {
361
+ if (!thinkingStartedAtRef.current) return;
362
+ setWorkingSeconds(Math.floor((Date.now() - thinkingStartedAtRef.current) / 1000));
363
+ }, 1000);
364
+
365
+ return () => clearInterval(timer);
366
+ }, [thinking]);
367
+
114
368
  useEffect(() => {
115
369
  fastModeRef.current = fastMode;
116
370
  }, [fastMode]);
@@ -131,12 +385,20 @@ async function createChatUI(options) {
131
385
  approvalChoiceRef.current = approvalChoice;
132
386
  }, [approvalChoice]);
133
387
 
388
+ useEffect(() => {
389
+ approvalSessionAutoApproveRef.current = approvalSessionAutoApprove;
390
+ }, [approvalSessionAutoApprove]);
391
+
134
392
  const showSuggestions = input.startsWith('/') && !input.includes(' ');
135
393
  const suggestions = useMemo(() => {
136
394
  if (!showSuggestions) return [];
137
395
  const query = input.toLowerCase();
138
396
  return SLASH_COMMANDS.filter(s => s.cmd.startsWith(query));
139
397
  }, [input, showSuggestions]);
398
+ const visibleSuggestions = useMemo(
399
+ () => getVisibleSuggestions(suggestions, selectedIndex),
400
+ [suggestions, selectedIndex]
401
+ );
140
402
 
141
403
  // Reset index when suggestions change
142
404
  useEffect(() => {
@@ -148,6 +410,7 @@ async function createChatUI(options) {
148
410
  // Export methods to the outside world via ref
149
411
  useImperativeHandle(ref, () => ({
150
412
  appendMessage: (role, text, metadata = {}) => {
413
+ if (!shouldAppendMessage(role, text)) return;
151
414
  setHistory(prev => [...prev, { role, text, time: new Date(), ...metadata }]);
152
415
  if (metadata.providerInfo) {
153
416
  const { provider, model } = metadata.providerInfo;
@@ -164,6 +427,7 @@ async function createChatUI(options) {
164
427
  }
165
428
  },
166
429
  appendAssistantStreamChunk: (chunk) => {
430
+ if (!String(chunk || '').trim()) return;
167
431
  const current = liveAssistantRef.current || { role: 'assistant', text: '', time: new Date() };
168
432
  const next = { ...current, text: `${current.text || ''}${chunk}` };
169
433
  liveAssistantRef.current = next;
@@ -177,7 +441,21 @@ async function createChatUI(options) {
177
441
  setHistory(prev => [...prev, current]);
178
442
  }
179
443
  },
180
- setThinking: (val) => setThinking(val),
444
+ setThinking: (val, seconds = 0) => {
445
+ if (val) {
446
+ const elapsed = Number.isFinite(seconds) ? Math.max(0, seconds) : 0;
447
+ if (!thinkingStartedAtRef.current) {
448
+ thinkingStartedAtRef.current = Date.now() - (elapsed * 1000);
449
+ }
450
+ setWorkingSeconds(Math.floor((Date.now() - thinkingStartedAtRef.current) / 1000));
451
+ setThinking(true);
452
+ return;
453
+ }
454
+
455
+ thinkingStartedAtRef.current = null;
456
+ setWorkingSeconds(0);
457
+ setThinking(false);
458
+ },
181
459
  setMode: (val) => setMode(val),
182
460
  setFastMode: (val) => {
183
461
  const next = Boolean(val);
@@ -192,7 +470,12 @@ async function createChatUI(options) {
192
470
  return next;
193
471
  },
194
472
  getFastMode: () => fastModeRef.current,
195
- setInputText: (val) => setInput(val || ''),
473
+ setInputText: (val) => {
474
+ const next = val || '';
475
+ inputRef.current = next;
476
+ setInput(next);
477
+ resetInputCursorToEnd();
478
+ },
196
479
  setPendingPasteText: (text) => {
197
480
  const normalized = normalizeInputText(text);
198
481
  setPendingPaste({ text: normalized, label: `[Pasted Content ${normalized.length} chars]` });
@@ -203,12 +486,13 @@ async function createChatUI(options) {
203
486
  updateWorkspace: (val) => setWorkspace(val),
204
487
  attachImage: (image) => {
205
488
  setPendingImages(prev => {
206
- if (prev.length === 0) {
207
- const prefix = normalizeInputText(inputRef.current).trim();
208
- setPendingImagePrefix(prefix);
209
- pendingImagePrefixRef.current = prefix;
210
- setInput('');
211
- }
489
+ const imageIndex = prev.length + 1;
490
+ setInput(current => {
491
+ const next = appendInlineImageToken(current, imageIndex);
492
+ inputRef.current = next;
493
+ resetInputCursorToEnd();
494
+ return next;
495
+ });
212
496
  return [...prev, image];
213
497
  });
214
498
  },
@@ -229,14 +513,39 @@ async function createChatUI(options) {
229
513
  if (action === 'memory_context' && process.env.MINT_SHOW_MEMORY_TRACE !== '1') {
230
514
  return;
231
515
  }
516
+ if (phase === 'tool_call') {
517
+ return;
518
+ }
232
519
  if (thought) {
520
+ if (process.env.MINT_HIDE_AGENT_NOTES === '1') {
521
+ return;
522
+ }
233
523
  text = thought;
234
- label = 'Thinking';
524
+ label = 'Working';
235
525
  labelColor = 'gray';
236
526
  isThought = true;
237
527
  } else if (action === 'thinking' || phase === 'thinking') {
238
528
  return;
239
529
  } else {
530
+ const activity = formatActivityStep(info);
531
+ if (activity) {
532
+ const fullText = `[${activity.title}] ${activity.detail}`;
533
+ if (fullText === lastSystemMessage.current) return;
534
+ lastSystemMessage.current = fullText;
535
+
536
+ setHistory(prev => [...prev, {
537
+ role: 'system',
538
+ label: activity.title,
539
+ labelColor: 'blueBright',
540
+ text: activity.detail,
541
+ isActivity: true,
542
+ activityTitle: activity.title,
543
+ activityDetail: activity.detail,
544
+ time: new Date()
545
+ }]);
546
+ return;
547
+ }
548
+
240
549
  label = action || phase || 'Action';
241
550
  text = target || message || '';
242
551
  if (!text) return;
@@ -263,11 +572,18 @@ async function createChatUI(options) {
263
572
  }]);
264
573
  },
265
574
  requestApproval: (request = {}) => {
575
+ if (approvalSessionAutoApproveRef.current) {
576
+ return Promise.resolve(true);
577
+ }
578
+
266
579
  return new Promise((resolve) => {
267
580
  const approval = {
268
581
  type: request.type || 'action',
269
582
  label: request.label || 'Requested action',
270
583
  preview: request.preview || '',
584
+ summary: request.summary || '',
585
+ openPath: request.openPath || '',
586
+ warnings: Array.isArray(request.warnings) ? request.warnings.filter(Boolean) : [],
271
587
  resolve
272
588
  };
273
589
  pendingApprovalRef.current = approval;
@@ -280,21 +596,41 @@ async function createChatUI(options) {
280
596
  useInput((inputStr, key) => {
281
597
  const approval = pendingApprovalRef.current;
282
598
  if (approval) {
283
- const resolveApproval = (approved) => {
599
+ const resolveApproval = (approved, approveForSession = false) => {
600
+ if (approveForSession) {
601
+ approvalSessionAutoApproveRef.current = true;
602
+ setApprovalSessionAutoApprove(true);
603
+ }
284
604
  pendingApprovalRef.current = null;
285
605
  setPendingApproval(null);
286
- setHistory(prev => [...prev, {
287
- role: 'system',
288
- label: 'Approval',
289
- labelColor: approved ? 'greenBright' : 'redBright',
290
- text: `${approved ? 'Approved' : 'Denied'}: ${approval.label}`,
291
- time: new Date()
292
- }]);
606
+ setHistory(prev => {
607
+ if (approved && isUnifiedDiffPreview(approval.preview)) {
608
+ return [...prev, {
609
+ role: 'system',
610
+ label: 'Edited',
611
+ labelColor: 'greenBright',
612
+ preview: approval.preview,
613
+ isDiffPreview: true,
614
+ time: new Date()
615
+ }];
616
+ }
617
+
618
+ return [...prev, {
619
+ role: 'system',
620
+ label: 'Approval',
621
+ labelColor: approved ? 'greenBright' : 'redBright',
622
+ text: `${approveForSession ? 'Approved this session' : (approved ? 'Approved' : 'Denied')}: ${approval.label}`,
623
+ time: new Date()
624
+ }];
625
+ });
293
626
  approval.resolve(approved);
294
627
  };
295
628
 
296
- if (key.leftArrow || key.rightArrow || key.tab) {
297
- const next = approvalChoiceRef.current === 'approve' ? 'deny' : 'approve';
629
+ if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.tab) {
630
+ const next = getNextApprovalChoice(
631
+ approvalChoiceRef.current,
632
+ (key.upArrow || key.leftArrow) ? -1 : 1
633
+ );
298
634
  approvalChoiceRef.current = next;
299
635
  setApprovalChoice(next);
300
636
  return;
@@ -302,13 +638,17 @@ async function createChatUI(options) {
302
638
 
303
639
  const answer = String(inputStr || '').toLowerCase();
304
640
  if (key.return) {
305
- resolveApproval(approvalChoiceRef.current === 'approve');
641
+ resolveApproval(approvalChoiceRef.current !== 'deny', approvalChoiceRef.current === 'approve_session');
306
642
  return;
307
643
  }
308
644
  if (answer === 'y') {
309
645
  resolveApproval(true);
310
646
  return;
311
647
  }
648
+ if (answer === 'a') {
649
+ resolveApproval(true, true);
650
+ return;
651
+ }
312
652
  if (answer === 'n' || key.escape || (key.ctrl && inputStr === 'c')) {
313
653
  resolveApproval(false);
314
654
  return;
@@ -319,8 +659,12 @@ async function createChatUI(options) {
319
659
  if (key.escape && pendingImagesRef.current.length > 0) {
320
660
  setPendingImages([]);
321
661
  pendingImagesRef.current = [];
322
- setPendingImagePrefix('');
323
- pendingImagePrefixRef.current = '';
662
+ setInput(current => {
663
+ const next = removeAllImageTokens(current);
664
+ inputRef.current = next;
665
+ resetInputCursorToEnd();
666
+ return next;
667
+ });
324
668
  return;
325
669
  }
326
670
 
@@ -329,16 +673,24 @@ async function createChatUI(options) {
329
673
  pendingPasteRef.current = null;
330
674
  setPendingPastePrefix('');
331
675
  pendingPastePrefixRef.current = '';
676
+ suppressPasteBurstRef.current = false;
677
+ inputRef.current = '';
678
+ setInput('');
679
+ resetInputCursorToEnd();
332
680
  return;
333
681
  }
334
682
 
335
683
  if (key.ctrl && key.backspace && pendingImagesRef.current.length > 0) {
336
- setPendingImages(prev => prev.slice(0, -1));
337
- pendingImagesRef.current = pendingImagesRef.current.slice(0, -1);
338
- if (pendingImagesRef.current.length === 0) {
339
- setPendingImagePrefix('');
340
- pendingImagePrefixRef.current = '';
341
- }
684
+ const imageIndex = pendingImagesRef.current.length;
685
+ const nextImages = pendingImagesRef.current.slice(0, -1);
686
+ pendingImagesRef.current = nextImages;
687
+ setPendingImages(nextImages);
688
+ setInput(current => {
689
+ const next = removeImageToken(current, imageIndex);
690
+ inputRef.current = next;
691
+ resetInputCursorToEnd();
692
+ return next;
693
+ });
342
694
  return;
343
695
  }
344
696
 
@@ -358,11 +710,14 @@ async function createChatUI(options) {
358
710
  .then((image) => {
359
711
  if (image) {
360
712
  setPendingImages(prev => {
361
- if (prev.length === 0) {
362
- const prefix = normalizeInputText(inputBeforePaste).trim();
363
- setPendingImagePrefix(prefix);
364
- pendingImagePrefixRef.current = prefix;
365
- }
713
+ const imageIndex = prev.length + 1;
714
+ setInput(current => {
715
+ const cleaned = removePasteArtifact(current);
716
+ const next = appendInlineImageToken(cleaned || inputBeforePaste, imageIndex);
717
+ inputRef.current = next;
718
+ resetInputCursorToEnd();
719
+ return next;
720
+ });
366
721
  return [...prev, image];
367
722
  });
368
723
  }
@@ -410,14 +765,13 @@ async function createChatUI(options) {
410
765
  const handleSubmit = (value) => {
411
766
  const text = normalizeInputText(value).trim();
412
767
  const images = pendingImagesRef.current;
413
- const imagePrefix = normalizeInputText(pendingImagePrefixRef.current).trim();
414
768
  const imageLabels = images.map((_, index) => `[Image #${index + 1}]`).join(' ');
415
769
  const pasted = pendingPasteRef.current;
416
770
  const pastePrefix = normalizeInputText(pendingPastePrefixRef.current).trim();
417
771
  const submittedText = pasted
418
772
  ? [pastePrefix, pasted.text, text].filter(Boolean).join('\n\n')
419
773
  : images.length > 0
420
- ? [imagePrefix, imageLabels, text].filter(Boolean).join('\n\n')
774
+ ? (text || imageLabels)
421
775
  : text;
422
776
  if (!submittedText && images.length === 0) return;
423
777
 
@@ -431,23 +785,35 @@ async function createChatUI(options) {
431
785
 
432
786
  setInput('');
433
787
  setPendingImages([]);
434
- setPendingImagePrefix('');
435
788
  setPendingPaste(null);
436
789
  setPendingPastePrefix('');
437
790
  pendingImagesRef.current = [];
438
- pendingImagePrefixRef.current = '';
439
791
  pendingPasteRef.current = null;
440
792
  pendingPastePrefixRef.current = '';
793
+ suppressPasteBurstRef.current = false;
441
794
  onSubmit(submittedText, { images, pasted });
442
795
  };
443
796
 
444
797
  const handleInputChange = (value) => {
798
+ if (suppressPasteBurstRef.current && pendingPasteRef.current) {
799
+ inputRef.current = '';
800
+ setInput('');
801
+ resetInputCursorToEnd();
802
+ return;
803
+ }
804
+
445
805
  if (shouldStoreAsPastedContent(value)) {
446
806
  const normalized = normalizeInputText(value);
447
807
  const previous = normalizeInputText(inputRef.current).trim();
448
- setPendingPaste({ text: normalized, label: `[Pasted Content ${normalized.length} chars]` });
808
+ const pasted = { text: normalized, label: `[Pasted Content ${normalized.length} chars]` };
809
+ pendingPasteRef.current = pasted;
810
+ pendingPastePrefixRef.current = previous;
811
+ suppressPasteBurstRef.current = true;
812
+ setPendingPaste(pasted);
449
813
  setPendingPastePrefix(previous);
814
+ inputRef.current = '';
450
815
  setInput('');
816
+ resetInputCursorToEnd();
451
817
  return;
452
818
  }
453
819
 
@@ -464,9 +830,73 @@ async function createChatUI(options) {
464
830
  return;
465
831
  }
466
832
  }
833
+ inputRef.current = normalizedValue;
467
834
  setInput(normalizedValue);
468
835
  };
469
836
 
837
+ const renderActivityDetail = (value) => {
838
+ const segments = splitDiffStatSegments(value);
839
+ return segments.map((segment, segmentIndex) =>
840
+ h(Text, {
841
+ key: `activity-detail-${segmentIndex}`,
842
+ color: segment.color,
843
+ wrap: 'wrap'
844
+ }, segment.text)
845
+ );
846
+ };
847
+
848
+ const renderDiffLine = (line, index) => {
849
+ const style = getDiffLineStyle(line);
850
+ return h(Text, {
851
+ key: `diff-line-${index}`,
852
+ ...style
853
+ }, line.text || ' ');
854
+ };
855
+
856
+ const renderDiffPreview = (preview) => {
857
+ const files = parseUnifiedDiffPreview(preview);
858
+ if (files.length === 0) return null;
859
+
860
+ return h(Box, { flexDirection: 'column', marginTop: 1 },
861
+ ...files.map((file, fileIndex) =>
862
+ h(Box, { key: `approval-diff-${fileIndex}`, flexDirection: 'column', marginBottom: 1 },
863
+ h(Box, null,
864
+ h(Text, { color: 'gray' }, '• '),
865
+ h(Text, { bold: true, color: 'white' }, `Edited ${file.path} `),
866
+ h(Text, { color: 'gray' }, '('),
867
+ h(Text, { color: 'greenBright' }, `+${file.additions}`),
868
+ h(Text, { color: 'gray' }, ' '),
869
+ h(Text, { color: 'redBright' }, `-${file.deletions}`),
870
+ h(Text, { color: 'gray' }, ')')
871
+ ),
872
+ h(Box, { flexDirection: 'column', paddingLeft: 2 },
873
+ ...file.lines.slice(0, 120).map(renderDiffLine),
874
+ file.lines.length > 120 && h(Text, { color: 'gray', dimColor: true }, `... ${file.lines.length - 120} more diff lines`)
875
+ )
876
+ )
877
+ )
878
+ );
879
+ };
880
+
881
+ const renderApprovalPreview = (approval) => {
882
+ const preview = approval && approval.preview ? approval.preview : '';
883
+ if (approval && approval.type === 'plan') {
884
+ return h(Box, { flexDirection: 'column', marginTop: 1 },
885
+ h(Text, { color: 'gray' }, approval.summary || 'Mint prepared a plan for this task.'),
886
+ approval.openPath && h(Text, { color: 'gray', dimColor: true }, `Details: ${approval.label}`)
887
+ );
888
+ }
889
+
890
+ const diffPreview = renderDiffPreview(preview);
891
+ if (!diffPreview) {
892
+ return preview && preview !== approval.label
893
+ ? h(Box, null, h(Text, { color: 'gray', dimColor: true }, preview))
894
+ : null;
895
+ }
896
+
897
+ return diffPreview;
898
+ };
899
+
470
900
  const renderMessage = (msg, index, keyPrefix = 'msg') => {
471
901
  if (msg.isThought) {
472
902
  return h(Box, { key: `${keyPrefix}-${index}`, flexDirection: 'row', marginBottom: 0, paddingLeft: 2 },
@@ -474,6 +904,25 @@ async function createChatUI(options) {
474
904
  );
475
905
  }
476
906
 
907
+ if (msg.isActivity) {
908
+ return h(Box, { key: `${keyPrefix}-${index}`, flexDirection: 'column', marginBottom: 0 },
909
+ h(Box, null,
910
+ h(Text, { color: 'greenBright' }, '• '),
911
+ h(Text, { bold: true, color: msg.labelColor || 'blueBright' }, msg.activityTitle || msg.label || 'Activity')
912
+ ),
913
+ h(Box, { paddingLeft: 2, marginBottom: 1 },
914
+ h(Text, { color: 'gray' }, '└ '),
915
+ ...renderActivityDetail(msg.activityDetail || msg.text)
916
+ )
917
+ );
918
+ }
919
+
920
+ if (msg.isDiffPreview) {
921
+ return h(Box, { key: `${keyPrefix}-${index}`, flexDirection: 'column', marginBottom: 0 },
922
+ renderDiffPreview(msg.preview || '')
923
+ );
924
+ }
925
+
477
926
  let name = 'Mint';
478
927
  let nameColor = 'greenBright';
479
928
 
@@ -494,7 +943,7 @@ async function createChatUI(options) {
494
943
  h(Text, { color: 'gray' }, ` ${msg.time instanceof Date ? msg.time.toLocaleTimeString() : ''}`)
495
944
  ),
496
945
  h(Box, { paddingLeft: 2, marginBottom: 1 },
497
- h(Text, null, msg.text)
946
+ h(Text, { wrap: 'wrap' }, cleanDisplayText(msg.text, msg.role))
498
947
  )
499
948
  );
500
949
  };
@@ -506,63 +955,75 @@ async function createChatUI(options) {
506
955
 
507
956
  // Floating (Persistent) UI part
508
957
  h(Box, { flexDirection: 'column' },
509
- thinking && h(Box, { marginBottom: 1 },
958
+ thinking && h(Box, { flexDirection: 'column', marginBottom: 1 },
959
+ h(Text, { color: 'gray', dimColor: true }, `─ Working for ${formatDuration(workingSeconds)} ─────────────────────────────────────────────────────────`),
510
960
  h(Text, { color: 'yellow' }, '● Mint is thinking...')
511
961
  ),
512
962
 
513
- // Suggestions Menu
514
- showSuggestions && suggestions.length > 0 && h(Box, {
515
- flexDirection: 'column',
516
- borderStyle: 'single',
517
- borderColor: 'gray',
963
+ pendingApproval && h(Box, {
964
+ flexDirection: 'column',
965
+ borderStyle: 'single',
966
+ borderColor: 'cyanBright',
518
967
  paddingX: 1,
519
968
  marginBottom: 0
520
969
  },
521
- suggestions.map((s, i) => h(Box, { key: s.cmd, flexDirection: 'row' },
522
- h(Text, {
523
- backgroundColor: i === selectedIndex ? 'green' : undefined,
524
- color: i === selectedIndex ? 'white' : 'greenBright'
525
- }, s.cmd.padEnd(12)),
526
- h(Text, { color: 'gray' }, ` ${s.desc}`)
527
- ))
970
+ h(Box, null,
971
+ pendingApproval.type === 'plan'
972
+ ? h(Text, { bold: true, color: 'greenBright' }, 'Plan')
973
+ : [
974
+ h(Text, { key: 'approval-title', bold: true, color: 'greenBright' }, 'Approval '),
975
+ h(Text, { key: 'approval-type', color: 'cyanBright' }, `[${pendingApproval.type}] `),
976
+ h(Text, { key: 'approval-label', color: 'white' }, pendingApproval.label)
977
+ ]
978
+ ),
979
+ pendingApproval.warnings && pendingApproval.warnings.length > 0 && h(Box, { flexDirection: 'column', marginTop: 1, marginBottom: 1 },
980
+ ...pendingApproval.warnings.map((warning, index) =>
981
+ h(Box, { key: `approval-warning-${index}` },
982
+ h(Text, { color: 'yellowBright' }, 'Warning: '),
983
+ h(Text, { color: 'yellowBright' }, warning)
984
+ )
985
+ )
986
+ ),
987
+ renderApprovalPreview(pendingApproval)
528
988
  ),
529
989
 
530
990
  pendingApproval && h(Box, {
531
991
  flexDirection: 'column',
532
992
  borderStyle: 'single',
533
- borderColor: 'yellow',
993
+ borderColor: approvalChoice === 'deny' ? 'redBright' : 'greenBright',
534
994
  paddingX: 1,
535
- marginBottom: 1
995
+ marginBottom: 0
536
996
  },
537
- h(Box, null,
538
- h(Text, { bold: true, color: 'yellow' }, 'Approval '),
539
- h(Text, { color: 'gray' }, `[${pendingApproval.type}] `),
540
- h(Text, { color: 'white' }, pendingApproval.label)
541
- ),
542
- pendingApproval.preview && pendingApproval.preview !== pendingApproval.label && h(Box, null,
543
- h(Text, { color: 'gray' }, pendingApproval.preview)
544
- ),
545
997
  h(Box, null,
546
998
  h(Text, {
547
999
  color: approvalChoice === 'approve' ? 'black' : 'greenBright',
548
1000
  backgroundColor: approvalChoice === 'approve' ? 'greenBright' : undefined,
549
1001
  bold: true
550
- }, ' Approve '),
551
- h(Text, { color: 'gray' }, ' '),
1002
+ }, approvalChoice === 'approve' ? '▸ Approve' : ' Approve')
1003
+ ),
1004
+ h(Box, null,
1005
+ h(Text, {
1006
+ color: approvalChoice === 'approve_session' ? 'black' : 'cyanBright',
1007
+ backgroundColor: approvalChoice === 'approve_session' ? 'cyanBright' : undefined,
1008
+ bold: true
1009
+ }, approvalChoice === 'approve_session' ? '▸ Approve Session' : ' Approve Session')
1010
+ ),
1011
+ h(Box, null,
552
1012
  h(Text, {
553
1013
  color: approvalChoice === 'deny' ? 'white' : 'redBright',
554
1014
  backgroundColor: approvalChoice === 'deny' ? 'redBright' : undefined,
555
1015
  bold: true
556
- }, ' Deny '),
557
- h(Text, { color: 'gray' }, ' Tab/←/→ Enter')
1016
+ }, approvalChoice === 'deny' ? '▸ Deny' : ' Deny')
1017
+ ),
1018
+ h(Box, null,
1019
+ h(Text, { color: 'gray', dimColor: true }, ' ↑/↓ Enter y/a/n')
558
1020
  )
559
1021
  ),
560
1022
 
561
1023
  // Compact Input Area
562
1024
  h(Box, { borderStyle: 'round', borderColor: pendingApproval ? 'gray' : 'greenBright', paddingX: 1, flexDirection: 'column' },
563
1025
  pendingImages.length > 0 && h(Box, null,
564
- pendingImagePrefix && h(Text, { color: 'cyanBright' }, '[Text before] '),
565
- h(Text, { color: 'greenBright' }, pendingImages.map((_, index) => `[Image #${index + 1}]`).join(' ') + ' '),
1026
+ h(Text, { color: 'greenBright' }, `${pendingImages.length} image${pendingImages.length === 1 ? '' : 's'} attached `),
566
1027
  h(Text, { color: 'gray' }, 'Enter to send, Ctrl+Backspace remove, Esc clear')
567
1028
  ),
568
1029
  pendingPaste && h(Box, null,
@@ -573,6 +1034,7 @@ async function createChatUI(options) {
573
1034
  h(Box, { flexDirection: 'row' },
574
1035
  h(Text, { bold: true, color: 'greenBright' }, '› '),
575
1036
  h(TextInput, {
1037
+ key: `input-${inputResetKey}`,
576
1038
  value: input,
577
1039
  onChange: pendingApproval ? () => {} : handleInputChange,
578
1040
  onSubmit: pendingApproval ? () => {} : handleSubmit,
@@ -581,11 +1043,36 @@ async function createChatUI(options) {
581
1043
  )
582
1044
  ),
583
1045
 
1046
+ // Suggestions Menu
1047
+ showSuggestions && suggestions.length > 0 && h(Box, {
1048
+ flexDirection: 'column',
1049
+ borderStyle: 'single',
1050
+ borderColor: 'gray',
1051
+ paddingX: 1,
1052
+ marginBottom: 0
1053
+ },
1054
+ h(Box, { justifyContent: 'space-between' },
1055
+ h(Text, { color: 'gray', dimColor: true }, 'Commands'),
1056
+ h(Text, { color: 'gray', dimColor: true }, `${visibleSuggestions.current}/${visibleSuggestions.total}`)
1057
+ ),
1058
+ visibleSuggestions.visible.map((s, i) => {
1059
+ const actualIndex = visibleSuggestions.start + i;
1060
+ return h(Box, { key: s.cmd, flexDirection: 'row' },
1061
+ h(Text, {
1062
+ backgroundColor: actualIndex === selectedIndex ? 'green' : undefined,
1063
+ color: actualIndex === selectedIndex ? 'white' : 'greenBright'
1064
+ }, s.cmd.padEnd(12)),
1065
+ h(Text, { color: 'gray' }, ` ${s.desc}`)
1066
+ );
1067
+ })
1068
+ ),
1069
+
584
1070
  // Status Bar
585
1071
  h(Box, { justifyContent: 'space-between' },
586
1072
  h(Box, null,
587
1073
  h(Text, { color: 'cyan' }, `[${fastMode ? 'Fast' : mode}] `),
588
- h(Text, { color: 'magentaBright' }, (model || config.geminiModel || 'gemini').slice(0, 46))
1074
+ h(Text, { color: 'magentaBright' }, (model || config.geminiModel || 'gemini').slice(0, 46)),
1075
+ approvalSessionAutoApprove && h(Text, { color: 'greenBright' }, ' approvals:session')
589
1076
  ),
590
1077
  h(Box, null,
591
1078
  h(Text, { color: 'gray' }, `path: ...${workspace.slice(-20)}`)
@@ -603,11 +1090,12 @@ async function createChatUI(options) {
603
1090
  console.log(`\x1b[90mType naturally to chat. Esc to exit.\x1b[0m\n`);
604
1091
 
605
1092
  const ref = createRef();
606
- render(h(App, { ref, ...options }), { exitOnCtrlC: false });
1093
+ const instance = render(h(App, { ref, ...options }), { exitOnCtrlC: false });
607
1094
 
608
1095
  return {
1096
+ unmount: () => instance.unmount(),
609
1097
  appendMessage: (role, text, metadata) => ref.current?.appendMessage(role, text, metadata),
610
- setThinking: (val) => ref.current?.setThinking(val),
1098
+ setThinking: (val, seconds) => ref.current?.setThinking(val, seconds),
611
1099
  setMode: (val) => ref.current?.setMode(val),
612
1100
  setFastMode: (val) => ref.current?.setFastMode(val),
613
1101
  toggleFastMode: () => ref.current?.toggleFastMode(),
@@ -619,11 +1107,10 @@ async function createChatUI(options) {
619
1107
  attachImage: (image) => ref.current?.attachImage(image),
620
1108
  appendCodeStep: (info) => ref.current?.appendCodeStep(info),
621
1109
  streamMessage: (metadata = {}) => {
622
- let fullText = '';
623
1110
  ref.current?.beginAssistantStream(metadata);
624
1111
  return {
625
1112
  appendChunk: (chunk) => {
626
- fullText += chunk;
1113
+ if (!String(chunk || '').trim()) return;
627
1114
  ref.current?.appendAssistantStreamChunk(chunk);
628
1115
  },
629
1116
  finalize: () => {
@@ -637,4 +1124,23 @@ async function createChatUI(options) {
637
1124
  };
638
1125
  }
639
1126
 
640
- module.exports = { createChatUI };
1127
+ module.exports = {
1128
+ createChatUI,
1129
+ _helpers: {
1130
+ cleanDisplayText,
1131
+ stripInlineMarkdown,
1132
+ compactPathLabel,
1133
+ formatActivityStep,
1134
+ formatDuration,
1135
+ splitDiffStatSegments,
1136
+ getNextApprovalChoice,
1137
+ getVisibleSuggestions,
1138
+ parseUnifiedDiffPreview,
1139
+ isUnifiedDiffPreview,
1140
+ getDiffLineStyle,
1141
+ shouldAppendMessage,
1142
+ appendInlineImageToken,
1143
+ removeImageToken,
1144
+ removeAllImageTokens
1145
+ }
1146
+ };