@pheem49/mint 1.5.2 → 1.5.4

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 (40) hide show
  1. package/GUIDE_TH.md +23 -11
  2. package/README.md +148 -66
  3. package/assets/Agent_Mint.png +0 -0
  4. package/assets/Settings.png +0 -0
  5. package/install.ps1 +64 -0
  6. package/install.sh +54 -0
  7. package/main.js +12 -0
  8. package/package.json +5 -3
  9. package/preload.js +4 -0
  10. package/scripts/install_linux_desktop_entry.js +48 -0
  11. package/src/AI_Brain/Gemini_API.js +231 -498
  12. package/src/AI_Brain/autonomous_brain.js +46 -19
  13. package/src/AI_Brain/headless_agent.js +21 -2
  14. package/src/AI_Brain/provider_adapter.js +358 -0
  15. package/src/Automation_Layer/file_operations.js +17 -5
  16. package/src/CLI/approval_handler.js +5 -0
  17. package/src/CLI/chat_router.js +7 -0
  18. package/src/CLI/chat_ui.js +397 -76
  19. package/src/CLI/cli_colors.js +86 -3
  20. package/src/CLI/cli_formatters.js +6 -1
  21. package/src/CLI/code_agent.js +706 -273
  22. package/src/CLI/interactive_chat.js +311 -149
  23. package/src/CLI/slash_command_handler.js +2 -2
  24. package/src/CLI/updater.js +21 -1
  25. package/src/System/config_manager.js +5 -1
  26. package/src/System/ipc_handlers.js +95 -1
  27. package/src/System/picture_store.js +109 -0
  28. package/src/System/smart_context.js +227 -0
  29. package/src/System/task_manager.js +127 -0
  30. package/src/System/tool_registry.js +13 -0
  31. package/src/System/window_manager.js +16 -8
  32. package/src/UI/live2d_manager.js +42 -8
  33. package/src/UI/preload-spotlight.js +1 -0
  34. package/src/UI/renderer.js +837 -63
  35. package/src/UI/settings.css +160 -96
  36. package/src/UI/settings.html +9 -0
  37. package/src/UI/settings.js +35 -2
  38. package/src/UI/spotlight.js +13 -9
  39. package/src/UI/styles.css +1592 -165
  40. package/privacy.txt +0 -1
@@ -63,6 +63,8 @@ function formatActivityStep(info = {}) {
63
63
  return { title: '⚠ Notice', detail: rawText };
64
64
  case 'run_shell':
65
65
  return { title: 'Ran', detail: rawText };
66
+ case 'plan':
67
+ return { title: 'Plan', detail: rawText };
66
68
  case 'apply_patch':
67
69
  case 'write_file':
68
70
  return { title: 'Edited', detail: rawText };
@@ -146,6 +148,107 @@ function formatDuration(totalSeconds) {
146
148
  return `${minutes}m ${remainingSeconds}s`;
147
149
  }
148
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
+
149
252
  function shouldAppendMessage(role, text) {
150
253
  if (role === 'assistant' || role === 'system') {
151
254
  return String(text || '').trim().length > 0;
@@ -153,6 +256,21 @@ function shouldAppendMessage(role, text) {
153
256
  return true;
154
257
  }
155
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
+
156
274
  /**
157
275
  * We wrap everything in an async function to load ESM modules
158
276
  */
@@ -175,26 +293,28 @@ async function createChatUI(options) {
175
293
  const [model, setModel] = useState('');
176
294
  const [workspace, setWorkspace] = useState(process.cwd());
177
295
  const [pendingImages, setPendingImages] = useState([]);
178
- const [pendingImagePrefix, setPendingImagePrefix] = useState('');
179
296
  const [pendingPaste, setPendingPaste] = useState(null);
180
297
  const [pendingPastePrefix, setPendingPastePrefix] = useState('');
181
298
  const [pendingApproval, setPendingApproval] = useState(null);
182
299
  const [approvalChoice, setApprovalChoice] = useState('approve');
300
+ const [approvalSessionAutoApprove, setApprovalSessionAutoApprove] = useState(false);
301
+ const [inputResetKey, setInputResetKey] = useState(0);
183
302
 
184
303
  // Suggestions State
185
304
  const [selectedIndex, setSelectedIndex] = useState(0);
186
305
  const inputRef = React.useRef(input);
187
306
  const pendingImagesRef = React.useRef(pendingImages);
188
- const pendingImagePrefixRef = React.useRef(pendingImagePrefix);
189
307
  const pendingPasteRef = React.useRef(pendingPaste);
190
308
  const pendingPastePrefixRef = React.useRef(pendingPastePrefix);
191
309
  const liveAssistantRef = React.useRef(liveAssistant);
192
310
  const thinkingStartedAtRef = React.useRef(null);
193
311
  const fastModeRef = React.useRef(fastMode);
194
312
  const suppressPasteCharRef = React.useRef(false);
313
+ const suppressPasteBurstRef = React.useRef(false);
195
314
  const selectedIndexRef = React.useRef(selectedIndex);
196
315
  const pendingApprovalRef = React.useRef(null);
197
316
  const approvalChoiceRef = React.useRef('approve');
317
+ const approvalSessionAutoApproveRef = React.useRef(false);
198
318
 
199
319
  const removePasteArtifact = (value) => {
200
320
  const text = String(value || '');
@@ -209,6 +329,10 @@ async function createChatUI(options) {
209
329
  const text = String(value || '');
210
330
  return text.length > 500 || /[\r\n]/.test(text);
211
331
  };
332
+
333
+ const resetInputCursorToEnd = () => {
334
+ setInputResetKey(key => key + 1);
335
+ };
212
336
 
213
337
  useEffect(() => {
214
338
  inputRef.current = input;
@@ -218,10 +342,6 @@ async function createChatUI(options) {
218
342
  pendingImagesRef.current = pendingImages;
219
343
  }, [pendingImages]);
220
344
 
221
- useEffect(() => {
222
- pendingImagePrefixRef.current = pendingImagePrefix;
223
- }, [pendingImagePrefix]);
224
-
225
345
  useEffect(() => {
226
346
  pendingPasteRef.current = pendingPaste;
227
347
  }, [pendingPaste]);
@@ -265,12 +385,20 @@ async function createChatUI(options) {
265
385
  approvalChoiceRef.current = approvalChoice;
266
386
  }, [approvalChoice]);
267
387
 
388
+ useEffect(() => {
389
+ approvalSessionAutoApproveRef.current = approvalSessionAutoApprove;
390
+ }, [approvalSessionAutoApprove]);
391
+
268
392
  const showSuggestions = input.startsWith('/') && !input.includes(' ');
269
393
  const suggestions = useMemo(() => {
270
394
  if (!showSuggestions) return [];
271
395
  const query = input.toLowerCase();
272
396
  return SLASH_COMMANDS.filter(s => s.cmd.startsWith(query));
273
397
  }, [input, showSuggestions]);
398
+ const visibleSuggestions = useMemo(
399
+ () => getVisibleSuggestions(suggestions, selectedIndex),
400
+ [suggestions, selectedIndex]
401
+ );
274
402
 
275
403
  // Reset index when suggestions change
276
404
  useEffect(() => {
@@ -342,7 +470,12 @@ async function createChatUI(options) {
342
470
  return next;
343
471
  },
344
472
  getFastMode: () => fastModeRef.current,
345
- setInputText: (val) => setInput(val || ''),
473
+ setInputText: (val) => {
474
+ const next = val || '';
475
+ inputRef.current = next;
476
+ setInput(next);
477
+ resetInputCursorToEnd();
478
+ },
346
479
  setPendingPasteText: (text) => {
347
480
  const normalized = normalizeInputText(text);
348
481
  setPendingPaste({ text: normalized, label: `[Pasted Content ${normalized.length} chars]` });
@@ -353,12 +486,13 @@ async function createChatUI(options) {
353
486
  updateWorkspace: (val) => setWorkspace(val),
354
487
  attachImage: (image) => {
355
488
  setPendingImages(prev => {
356
- if (prev.length === 0) {
357
- const prefix = normalizeInputText(inputRef.current).trim();
358
- setPendingImagePrefix(prefix);
359
- pendingImagePrefixRef.current = prefix;
360
- setInput('');
361
- }
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
+ });
362
496
  return [...prev, image];
363
497
  });
364
498
  },
@@ -379,12 +513,15 @@ async function createChatUI(options) {
379
513
  if (action === 'memory_context' && process.env.MINT_SHOW_MEMORY_TRACE !== '1') {
380
514
  return;
381
515
  }
516
+ if (phase === 'tool_call') {
517
+ return;
518
+ }
382
519
  if (thought) {
383
- if (process.env.MINT_SHOW_THINKING_TRACE !== '1') {
520
+ if (process.env.MINT_HIDE_AGENT_NOTES === '1') {
384
521
  return;
385
522
  }
386
523
  text = thought;
387
- label = 'Thinking';
524
+ label = 'Working';
388
525
  labelColor = 'gray';
389
526
  isThought = true;
390
527
  } else if (action === 'thinking' || phase === 'thinking') {
@@ -435,11 +572,18 @@ async function createChatUI(options) {
435
572
  }]);
436
573
  },
437
574
  requestApproval: (request = {}) => {
575
+ if (approvalSessionAutoApproveRef.current) {
576
+ return Promise.resolve(true);
577
+ }
578
+
438
579
  return new Promise((resolve) => {
439
580
  const approval = {
440
581
  type: request.type || 'action',
441
582
  label: request.label || 'Requested action',
442
583
  preview: request.preview || '',
584
+ summary: request.summary || '',
585
+ openPath: request.openPath || '',
586
+ warnings: Array.isArray(request.warnings) ? request.warnings.filter(Boolean) : [],
443
587
  resolve
444
588
  };
445
589
  pendingApprovalRef.current = approval;
@@ -452,21 +596,41 @@ async function createChatUI(options) {
452
596
  useInput((inputStr, key) => {
453
597
  const approval = pendingApprovalRef.current;
454
598
  if (approval) {
455
- const resolveApproval = (approved) => {
599
+ const resolveApproval = (approved, approveForSession = false) => {
600
+ if (approveForSession) {
601
+ approvalSessionAutoApproveRef.current = true;
602
+ setApprovalSessionAutoApprove(true);
603
+ }
456
604
  pendingApprovalRef.current = null;
457
605
  setPendingApproval(null);
458
- setHistory(prev => [...prev, {
459
- role: 'system',
460
- label: 'Approval',
461
- labelColor: approved ? 'greenBright' : 'redBright',
462
- text: `${approved ? 'Approved' : 'Denied'}: ${approval.label}`,
463
- time: new Date()
464
- }]);
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
+ });
465
626
  approval.resolve(approved);
466
627
  };
467
628
 
468
- if (key.leftArrow || key.rightArrow || key.tab) {
469
- 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
+ );
470
634
  approvalChoiceRef.current = next;
471
635
  setApprovalChoice(next);
472
636
  return;
@@ -474,13 +638,17 @@ async function createChatUI(options) {
474
638
 
475
639
  const answer = String(inputStr || '').toLowerCase();
476
640
  if (key.return) {
477
- resolveApproval(approvalChoiceRef.current === 'approve');
641
+ resolveApproval(approvalChoiceRef.current !== 'deny', approvalChoiceRef.current === 'approve_session');
478
642
  return;
479
643
  }
480
644
  if (answer === 'y') {
481
645
  resolveApproval(true);
482
646
  return;
483
647
  }
648
+ if (answer === 'a') {
649
+ resolveApproval(true, true);
650
+ return;
651
+ }
484
652
  if (answer === 'n' || key.escape || (key.ctrl && inputStr === 'c')) {
485
653
  resolveApproval(false);
486
654
  return;
@@ -491,8 +659,12 @@ async function createChatUI(options) {
491
659
  if (key.escape && pendingImagesRef.current.length > 0) {
492
660
  setPendingImages([]);
493
661
  pendingImagesRef.current = [];
494
- setPendingImagePrefix('');
495
- pendingImagePrefixRef.current = '';
662
+ setInput(current => {
663
+ const next = removeAllImageTokens(current);
664
+ inputRef.current = next;
665
+ resetInputCursorToEnd();
666
+ return next;
667
+ });
496
668
  return;
497
669
  }
498
670
 
@@ -501,16 +673,24 @@ async function createChatUI(options) {
501
673
  pendingPasteRef.current = null;
502
674
  setPendingPastePrefix('');
503
675
  pendingPastePrefixRef.current = '';
676
+ suppressPasteBurstRef.current = false;
677
+ inputRef.current = '';
678
+ setInput('');
679
+ resetInputCursorToEnd();
504
680
  return;
505
681
  }
506
682
 
507
683
  if (key.ctrl && key.backspace && pendingImagesRef.current.length > 0) {
508
- setPendingImages(prev => prev.slice(0, -1));
509
- pendingImagesRef.current = pendingImagesRef.current.slice(0, -1);
510
- if (pendingImagesRef.current.length === 0) {
511
- setPendingImagePrefix('');
512
- pendingImagePrefixRef.current = '';
513
- }
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
+ });
514
694
  return;
515
695
  }
516
696
 
@@ -530,11 +710,14 @@ async function createChatUI(options) {
530
710
  .then((image) => {
531
711
  if (image) {
532
712
  setPendingImages(prev => {
533
- if (prev.length === 0) {
534
- const prefix = normalizeInputText(inputBeforePaste).trim();
535
- setPendingImagePrefix(prefix);
536
- pendingImagePrefixRef.current = prefix;
537
- }
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
+ });
538
721
  return [...prev, image];
539
722
  });
540
723
  }
@@ -582,14 +765,13 @@ async function createChatUI(options) {
582
765
  const handleSubmit = (value) => {
583
766
  const text = normalizeInputText(value).trim();
584
767
  const images = pendingImagesRef.current;
585
- const imagePrefix = normalizeInputText(pendingImagePrefixRef.current).trim();
586
768
  const imageLabels = images.map((_, index) => `[Image #${index + 1}]`).join(' ');
587
769
  const pasted = pendingPasteRef.current;
588
770
  const pastePrefix = normalizeInputText(pendingPastePrefixRef.current).trim();
589
771
  const submittedText = pasted
590
772
  ? [pastePrefix, pasted.text, text].filter(Boolean).join('\n\n')
591
773
  : images.length > 0
592
- ? [imagePrefix, imageLabels, text].filter(Boolean).join('\n\n')
774
+ ? (text || imageLabels)
593
775
  : text;
594
776
  if (!submittedText && images.length === 0) return;
595
777
 
@@ -603,23 +785,35 @@ async function createChatUI(options) {
603
785
 
604
786
  setInput('');
605
787
  setPendingImages([]);
606
- setPendingImagePrefix('');
607
788
  setPendingPaste(null);
608
789
  setPendingPastePrefix('');
609
790
  pendingImagesRef.current = [];
610
- pendingImagePrefixRef.current = '';
611
791
  pendingPasteRef.current = null;
612
792
  pendingPastePrefixRef.current = '';
793
+ suppressPasteBurstRef.current = false;
613
794
  onSubmit(submittedText, { images, pasted });
614
795
  };
615
796
 
616
797
  const handleInputChange = (value) => {
798
+ if (suppressPasteBurstRef.current && pendingPasteRef.current) {
799
+ inputRef.current = '';
800
+ setInput('');
801
+ resetInputCursorToEnd();
802
+ return;
803
+ }
804
+
617
805
  if (shouldStoreAsPastedContent(value)) {
618
806
  const normalized = normalizeInputText(value);
619
807
  const previous = normalizeInputText(inputRef.current).trim();
620
- 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);
621
813
  setPendingPastePrefix(previous);
814
+ inputRef.current = '';
622
815
  setInput('');
816
+ resetInputCursorToEnd();
623
817
  return;
624
818
  }
625
819
 
@@ -636,9 +830,73 @@ async function createChatUI(options) {
636
830
  return;
637
831
  }
638
832
  }
833
+ inputRef.current = normalizedValue;
639
834
  setInput(normalizedValue);
640
835
  };
641
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
+
642
900
  const renderMessage = (msg, index, keyPrefix = 'msg') => {
643
901
  if (msg.isThought) {
644
902
  return h(Box, { key: `${keyPrefix}-${index}`, flexDirection: 'row', marginBottom: 0, paddingLeft: 2 },
@@ -654,11 +912,17 @@ async function createChatUI(options) {
654
912
  ),
655
913
  h(Box, { paddingLeft: 2, marginBottom: 1 },
656
914
  h(Text, { color: 'gray' }, '└ '),
657
- h(Text, { color: 'cyanBright', wrap: 'wrap' }, msg.activityDetail || msg.text)
915
+ ...renderActivityDetail(msg.activityDetail || msg.text)
658
916
  )
659
917
  );
660
918
  }
661
919
 
920
+ if (msg.isDiffPreview) {
921
+ return h(Box, { key: `${keyPrefix}-${index}`, flexDirection: 'column', marginBottom: 0 },
922
+ renderDiffPreview(msg.preview || '')
923
+ );
924
+ }
925
+
662
926
  let name = 'Mint';
663
927
  let nameColor = 'greenBright';
664
928
 
@@ -696,59 +960,70 @@ async function createChatUI(options) {
696
960
  h(Text, { color: 'yellow' }, '● Mint is thinking...')
697
961
  ),
698
962
 
699
- // Suggestions Menu
700
- showSuggestions && suggestions.length > 0 && h(Box, {
701
- flexDirection: 'column',
702
- borderStyle: 'single',
703
- borderColor: 'gray',
963
+ pendingApproval && h(Box, {
964
+ flexDirection: 'column',
965
+ borderStyle: 'single',
966
+ borderColor: 'cyanBright',
704
967
  paddingX: 1,
705
968
  marginBottom: 0
706
969
  },
707
- suggestions.map((s, i) => h(Box, { key: s.cmd, flexDirection: 'row' },
708
- h(Text, {
709
- backgroundColor: i === selectedIndex ? 'green' : undefined,
710
- color: i === selectedIndex ? 'white' : 'greenBright'
711
- }, s.cmd.padEnd(12)),
712
- h(Text, { color: 'gray' }, ` ${s.desc}`)
713
- ))
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)
714
988
  ),
715
989
 
716
990
  pendingApproval && h(Box, {
717
991
  flexDirection: 'column',
718
992
  borderStyle: 'single',
719
- borderColor: 'yellow',
993
+ borderColor: approvalChoice === 'deny' ? 'redBright' : 'greenBright',
720
994
  paddingX: 1,
721
- marginBottom: 1
995
+ marginBottom: 0
722
996
  },
723
- h(Box, null,
724
- h(Text, { bold: true, color: 'yellow' }, 'Approval '),
725
- h(Text, { color: 'gray' }, `[${pendingApproval.type}] `),
726
- h(Text, { color: 'white' }, pendingApproval.label)
727
- ),
728
- pendingApproval.preview && pendingApproval.preview !== pendingApproval.label && h(Box, null,
729
- h(Text, { color: 'gray' }, pendingApproval.preview)
730
- ),
731
997
  h(Box, null,
732
998
  h(Text, {
733
999
  color: approvalChoice === 'approve' ? 'black' : 'greenBright',
734
1000
  backgroundColor: approvalChoice === 'approve' ? 'greenBright' : undefined,
735
1001
  bold: true
736
- }, ' Approve '),
737
- 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,
738
1012
  h(Text, {
739
1013
  color: approvalChoice === 'deny' ? 'white' : 'redBright',
740
1014
  backgroundColor: approvalChoice === 'deny' ? 'redBright' : undefined,
741
1015
  bold: true
742
- }, ' Deny '),
743
- 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')
744
1020
  )
745
1021
  ),
746
1022
 
747
1023
  // Compact Input Area
748
1024
  h(Box, { borderStyle: 'round', borderColor: pendingApproval ? 'gray' : 'greenBright', paddingX: 1, flexDirection: 'column' },
749
1025
  pendingImages.length > 0 && h(Box, null,
750
- pendingImagePrefix && h(Text, { color: 'cyanBright' }, '[Text before] '),
751
- h(Text, { color: 'greenBright' }, pendingImages.map((_, index) => `[Image #${index + 1}]`).join(' ') + ' '),
1026
+ h(Text, { color: 'greenBright' }, `${pendingImages.length} image${pendingImages.length === 1 ? '' : 's'} attached `),
752
1027
  h(Text, { color: 'gray' }, 'Enter to send, Ctrl+Backspace remove, Esc clear')
753
1028
  ),
754
1029
  pendingPaste && h(Box, null,
@@ -759,6 +1034,7 @@ async function createChatUI(options) {
759
1034
  h(Box, { flexDirection: 'row' },
760
1035
  h(Text, { bold: true, color: 'greenBright' }, '› '),
761
1036
  h(TextInput, {
1037
+ key: `input-${inputResetKey}`,
762
1038
  value: input,
763
1039
  onChange: pendingApproval ? () => {} : handleInputChange,
764
1040
  onSubmit: pendingApproval ? () => {} : handleSubmit,
@@ -767,11 +1043,36 @@ async function createChatUI(options) {
767
1043
  )
768
1044
  ),
769
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
+
770
1070
  // Status Bar
771
1071
  h(Box, { justifyContent: 'space-between' },
772
1072
  h(Box, null,
773
1073
  h(Text, { color: 'cyan' }, `[${fastMode ? 'Fast' : mode}] `),
774
- 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')
775
1076
  ),
776
1077
  h(Box, null,
777
1078
  h(Text, { color: 'gray' }, `path: ...${workspace.slice(-20)}`)
@@ -789,9 +1090,10 @@ async function createChatUI(options) {
789
1090
  console.log(`\x1b[90mType naturally to chat. Esc to exit.\x1b[0m\n`);
790
1091
 
791
1092
  const ref = createRef();
792
- render(h(App, { ref, ...options }), { exitOnCtrlC: false });
1093
+ const instance = render(h(App, { ref, ...options }), { exitOnCtrlC: false });
793
1094
 
794
1095
  return {
1096
+ unmount: () => instance.unmount(),
795
1097
  appendMessage: (role, text, metadata) => ref.current?.appendMessage(role, text, metadata),
796
1098
  setThinking: (val, seconds) => ref.current?.setThinking(val, seconds),
797
1099
  setMode: (val) => ref.current?.setMode(val),
@@ -822,4 +1124,23 @@ async function createChatUI(options) {
822
1124
  };
823
1125
  }
824
1126
 
825
- module.exports = { createChatUI, _helpers: { cleanDisplayText, stripInlineMarkdown, compactPathLabel, formatActivityStep, formatDuration, shouldAppendMessage } };
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
+ };