@pheem49/mint 1.5.0 → 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 (101) hide show
  1. package/README.md +35 -1
  2. package/main.js +28 -14
  3. package/mint-cli-logic.js +3 -119
  4. package/mint-cli.js +201 -500
  5. package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  6. package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +40 -0
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json +15 -0
  9. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
  10. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
  11. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
  12. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +15 -0
  13. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
  14. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
  15. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
  16. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
  17. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
  18. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
  19. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
  20. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
  21. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
  22. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
  23. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
  24. package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +23 -0
  25. package/package.json +40 -17
  26. package/src/AI_Brain/Gemini_API.js +147 -46
  27. package/src/AI_Brain/autonomous_brain.js +2 -1
  28. package/src/AI_Brain/memory_store.js +299 -3
  29. package/src/AI_Brain/proactive_engine.js +12 -2
  30. package/src/Automation_Layer/browser_automation.js +26 -24
  31. package/src/CLI/approval_handler.js +42 -0
  32. package/src/CLI/chat_router.js +18 -6
  33. package/src/CLI/chat_ui.js +583 -52
  34. package/src/CLI/cli_colors.js +32 -0
  35. package/src/CLI/cli_formatters.js +89 -0
  36. package/src/CLI/code_agent.js +369 -71
  37. package/src/CLI/image_input.js +90 -0
  38. package/src/CLI/intent_detectors.js +181 -0
  39. package/src/CLI/interactive_chat.js +479 -0
  40. package/src/CLI/list_features.js +3 -0
  41. package/src/CLI/onboarding.js +72 -15
  42. package/src/CLI/repo_summarizer.js +282 -0
  43. package/src/CLI/semantic_code_search.js +312 -0
  44. package/src/CLI/skill_manager.js +41 -0
  45. package/src/CLI/slash_command_handler.js +418 -0
  46. package/src/CLI/symbol_indexer.js +231 -0
  47. package/src/CLI/updater.js +6 -4
  48. package/src/Channels/discord_bridge.js +11 -13
  49. package/src/Channels/line_bridge.js +10 -10
  50. package/src/Channels/slack_bridge.js +7 -12
  51. package/src/Channels/telegram_bridge.js +6 -14
  52. package/src/Channels/whatsapp_bridge.js +11 -9
  53. package/src/System/action_executor.js +59 -10
  54. package/src/System/chat_history_manager.js +20 -12
  55. package/src/System/config_manager.js +31 -1
  56. package/src/System/granular_automation.js +122 -53
  57. package/src/System/optional_require.js +23 -0
  58. package/src/System/proactive_loop.js +19 -3
  59. package/src/System/safety_manager.js +108 -0
  60. package/src/System/sandbox_runner.js +182 -0
  61. package/src/System/system_automation.js +127 -81
  62. package/src/System/system_info.js +70 -0
  63. package/src/System/tool_registry.js +280 -0
  64. package/src/System/window_manager.js +4 -2
  65. package/src/UI/live2d_manager.js +566 -0
  66. package/src/UI/renderer.js +339 -21
  67. package/src/UI/settings.css +655 -420
  68. package/src/UI/settings.html +478 -432
  69. package/src/UI/settings.js +10 -8
  70. package/src/UI/styles.css +516 -31
  71. package/.codex +0 -0
  72. package/docs/assets/Agent_Mint.png +0 -0
  73. package/docs/assets/CLI_Screen.png +0 -0
  74. package/docs/assets/Settings.png +0 -0
  75. package/docs/assets/icon.png +0 -0
  76. package/docs/guide.html +0 -632
  77. package/docs/index.html +0 -133
  78. package/docs/style.css +0 -579
  79. package/index.html +0 -16
  80. package/src/UI/index.html +0 -126
  81. package/tech_news.txt +0 -3
  82. package/test_knowledge.txt +0 -3
  83. package/tests/action_executor_safety.test.js +0 -67
  84. package/tests/agent_orchestrator.test.js +0 -41
  85. package/tests/chat_router.test.js +0 -42
  86. package/tests/code_agent.test.js +0 -69
  87. package/tests/config_manager.test.js +0 -141
  88. package/tests/docker.test.js +0 -46
  89. package/tests/file_operations.test.js +0 -57
  90. package/tests/gmail.test.js +0 -135
  91. package/tests/gmail_auth.test.js +0 -129
  92. package/tests/google_calendar.test.js +0 -113
  93. package/tests/google_tts_urls.test.js +0 -24
  94. package/tests/memory_store.test.js +0 -185
  95. package/tests/notion.test.js +0 -121
  96. package/tests/provider_routing.test.js +0 -83
  97. package/tests/safety_manager.test.js +0 -40
  98. package/tests/spotify.test.js +0 -201
  99. package/tests/system_monitor.test.js +0 -37
  100. package/tests/updater.test.js +0 -32
  101. package/tests/workspace_manager.test.js +0 -56
@@ -12,9 +12,15 @@ const h = React.createElement;
12
12
 
13
13
  const SLASH_COMMANDS = [
14
14
  { cmd: '/help', desc: 'Show available commands' },
15
+ { cmd: '/image', desc: 'Attach an image from a file path' },
16
+ { cmd: '/paste', desc: 'Attach an image from the clipboard' },
17
+ { cmd: '/fast', desc: 'Toggle fast mode (hide thinking)' },
18
+ { cmd: '/learn', desc: 'Remember a markdown skill file' },
15
19
  { cmd: '/code', desc: 'Force workspace Code Mode' },
16
20
  { cmd: '/cd', desc: 'Change current working directory' },
17
21
  { cmd: '/models', desc: 'List or switch Gemini models' },
22
+ { cmd: '/memory', desc: 'List, search, clear, or export long-term memory' },
23
+ { cmd: '/memory skills', desc: 'Show learned skill files' },
18
24
  { cmd: '/config', desc: 'Show current configuration' },
19
25
  { cmd: '/copy', desc: 'Copy last response to clipboard' },
20
26
  { cmd: '/clear', desc: 'Clear conversation history' },
@@ -26,6 +32,127 @@ const SLASH_COMMANDS = [
26
32
  { cmd: '/exit', desc: 'Exit Mint' }
27
33
  ];
28
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
+
29
156
  /**
30
157
  * We wrap everything in an async function to load ESM modules
31
158
  */
@@ -35,29 +162,109 @@ async function createChatUI(options) {
35
162
  const TextInput = (await import('ink-text-input')).default;
36
163
  const { useState, useImperativeHandle, forwardRef, createRef, useEffect, useMemo } = React;
37
164
 
38
- const App = forwardRef(({ onSubmit, onExit, initialHistory = [] }, ref) => {
165
+ const App = forwardRef(({ onSubmit, onExit, onPasteImage, initialHistory = [] }, ref) => {
39
166
  const config = readConfig();
40
167
  const { exit } = useApp();
41
168
  const [input, setInput] = useState('');
42
169
  const [history, setHistory] = useState(initialHistory);
170
+ const [liveAssistant, setLiveAssistant] = useState(null);
43
171
  const [thinking, setThinking] = useState(false);
44
- const [mode, setMode] = useState('Chat');
172
+ const [workingSeconds, setWorkingSeconds] = useState(0);
173
+ const [fastMode, setFastMode] = useState(false);
174
+ const [mode, setMode] = useState('Agent');
45
175
  const [model, setModel] = useState('');
46
176
  const [workspace, setWorkspace] = useState(process.cwd());
177
+ const [pendingImages, setPendingImages] = useState([]);
178
+ const [pendingImagePrefix, setPendingImagePrefix] = useState('');
179
+ const [pendingPaste, setPendingPaste] = useState(null);
180
+ const [pendingPastePrefix, setPendingPastePrefix] = useState('');
181
+ const [pendingApproval, setPendingApproval] = useState(null);
182
+ const [approvalChoice, setApprovalChoice] = useState('approve');
47
183
 
48
184
  // Suggestions State
49
185
  const [selectedIndex, setSelectedIndex] = useState(0);
50
186
  const inputRef = React.useRef(input);
187
+ const pendingImagesRef = React.useRef(pendingImages);
188
+ const pendingImagePrefixRef = React.useRef(pendingImagePrefix);
189
+ const pendingPasteRef = React.useRef(pendingPaste);
190
+ const pendingPastePrefixRef = React.useRef(pendingPastePrefix);
191
+ const liveAssistantRef = React.useRef(liveAssistant);
192
+ const thinkingStartedAtRef = React.useRef(null);
193
+ const fastModeRef = React.useRef(fastMode);
194
+ const suppressPasteCharRef = React.useRef(false);
51
195
  const selectedIndexRef = React.useRef(selectedIndex);
196
+ const pendingApprovalRef = React.useRef(null);
197
+ const approvalChoiceRef = React.useRef('approve');
198
+
199
+ const removePasteArtifact = (value) => {
200
+ const text = String(value || '');
201
+ return text.replace(/[vV]$/, '');
202
+ };
203
+
204
+ const normalizeInputText = (value) => {
205
+ return String(value || '').replace(/\s*[\r\n]+\s*/g, ' ');
206
+ };
207
+
208
+ const shouldStoreAsPastedContent = (value) => {
209
+ const text = String(value || '');
210
+ return text.length > 500 || /[\r\n]/.test(text);
211
+ };
52
212
 
53
213
  useEffect(() => {
54
214
  inputRef.current = input;
55
215
  }, [input]);
56
216
 
217
+ useEffect(() => {
218
+ pendingImagesRef.current = pendingImages;
219
+ }, [pendingImages]);
220
+
221
+ useEffect(() => {
222
+ pendingImagePrefixRef.current = pendingImagePrefix;
223
+ }, [pendingImagePrefix]);
224
+
225
+ useEffect(() => {
226
+ pendingPasteRef.current = pendingPaste;
227
+ }, [pendingPaste]);
228
+
229
+ useEffect(() => {
230
+ pendingPastePrefixRef.current = pendingPastePrefix;
231
+ }, [pendingPastePrefix]);
232
+
233
+ useEffect(() => {
234
+ liveAssistantRef.current = liveAssistant;
235
+ }, [liveAssistant]);
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
+
248
+ useEffect(() => {
249
+ fastModeRef.current = fastMode;
250
+ }, [fastMode]);
251
+
57
252
  useEffect(() => {
58
253
  selectedIndexRef.current = selectedIndex;
59
254
  }, [selectedIndex]);
60
255
 
256
+ useEffect(() => {
257
+ pendingApprovalRef.current = pendingApproval;
258
+ if (pendingApproval) {
259
+ approvalChoiceRef.current = 'approve';
260
+ setApprovalChoice('approve');
261
+ }
262
+ }, [pendingApproval]);
263
+
264
+ useEffect(() => {
265
+ approvalChoiceRef.current = approvalChoice;
266
+ }, [approvalChoice]);
267
+
61
268
  const showSuggestions = input.startsWith('/') && !input.includes(' ');
62
269
  const suggestions = useMemo(() => {
63
270
  if (!showSuggestions) return [];
@@ -75,17 +282,91 @@ async function createChatUI(options) {
75
282
  // Export methods to the outside world via ref
76
283
  useImperativeHandle(ref, () => ({
77
284
  appendMessage: (role, text, metadata = {}) => {
285
+ if (!shouldAppendMessage(role, text)) return;
78
286
  setHistory(prev => [...prev, { role, text, time: new Date(), ...metadata }]);
79
287
  if (metadata.providerInfo) {
80
288
  const { provider, model } = metadata.providerInfo;
81
289
  setModel(model ? `${provider} • ${model}` : provider);
82
290
  }
83
291
  },
84
- setThinking: (val) => setThinking(val),
292
+ beginAssistantStream: (metadata = {}) => {
293
+ const msg = { role: 'assistant', text: '', time: new Date(), ...metadata };
294
+ liveAssistantRef.current = msg;
295
+ setLiveAssistant(msg);
296
+ if (metadata.providerInfo) {
297
+ const { provider, model } = metadata.providerInfo;
298
+ setModel(model ? `${provider} • ${model}` : provider);
299
+ }
300
+ },
301
+ appendAssistantStreamChunk: (chunk) => {
302
+ if (!String(chunk || '').trim()) return;
303
+ const current = liveAssistantRef.current || { role: 'assistant', text: '', time: new Date() };
304
+ const next = { ...current, text: `${current.text || ''}${chunk}` };
305
+ liveAssistantRef.current = next;
306
+ setLiveAssistant(next);
307
+ },
308
+ finalizeAssistantStream: () => {
309
+ const current = liveAssistantRef.current;
310
+ liveAssistantRef.current = null;
311
+ setLiveAssistant(null);
312
+ if (current && String(current.text || '').trim()) {
313
+ setHistory(prev => [...prev, current]);
314
+ }
315
+ },
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
+ },
85
331
  setMode: (val) => setMode(val),
332
+ setFastMode: (val) => {
333
+ const next = Boolean(val);
334
+ fastModeRef.current = next;
335
+ setFastMode(next);
336
+ return next;
337
+ },
338
+ toggleFastMode: () => {
339
+ const next = !fastModeRef.current;
340
+ fastModeRef.current = next;
341
+ setFastMode(next);
342
+ return next;
343
+ },
344
+ getFastMode: () => fastModeRef.current,
345
+ setInputText: (val) => setInput(val || ''),
346
+ setPendingPasteText: (text) => {
347
+ const normalized = normalizeInputText(text);
348
+ setPendingPaste({ text: normalized, label: `[Pasted Content ${normalized.length} chars]` });
349
+ setPendingPastePrefix('');
350
+ setInput('');
351
+ },
86
352
  updateStatusModel: (val) => setModel(val),
87
353
  updateWorkspace: (val) => setWorkspace(val),
354
+ attachImage: (image) => {
355
+ setPendingImages(prev => {
356
+ if (prev.length === 0) {
357
+ const prefix = normalizeInputText(inputRef.current).trim();
358
+ setPendingImagePrefix(prefix);
359
+ pendingImagePrefixRef.current = prefix;
360
+ setInput('');
361
+ }
362
+ return [...prev, image];
363
+ });
364
+ },
88
365
  appendCodeStep: (info) => {
366
+ if (fastModeRef.current) {
367
+ return;
368
+ }
369
+
89
370
  let text = '';
90
371
  let label = 'System';
91
372
  let labelColor = 'blueBright';
@@ -95,7 +376,13 @@ async function createChatUI(options) {
95
376
  text = info;
96
377
  } else {
97
378
  const { action, phase, target, message, thought } = info;
379
+ if (action === 'memory_context' && process.env.MINT_SHOW_MEMORY_TRACE !== '1') {
380
+ return;
381
+ }
98
382
  if (thought) {
383
+ if (process.env.MINT_SHOW_THINKING_TRACE !== '1') {
384
+ return;
385
+ }
99
386
  text = thought;
100
387
  label = 'Thinking';
101
388
  labelColor = 'gray';
@@ -103,6 +390,25 @@ async function createChatUI(options) {
103
390
  } else if (action === 'thinking' || phase === 'thinking') {
104
391
  return;
105
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
+
106
412
  label = action || phase || 'Action';
107
413
  text = target || message || '';
108
414
  if (!text) return;
@@ -127,17 +433,131 @@ async function createChatUI(options) {
127
433
  isThought,
128
434
  time: new Date()
129
435
  }]);
436
+ },
437
+ requestApproval: (request = {}) => {
438
+ return new Promise((resolve) => {
439
+ const approval = {
440
+ type: request.type || 'action',
441
+ label: request.label || 'Requested action',
442
+ preview: request.preview || '',
443
+ resolve
444
+ };
445
+ pendingApprovalRef.current = approval;
446
+ setPendingApproval(approval);
447
+ });
130
448
  }
131
449
  }));
132
450
 
133
451
  // Handle exiting and keyboard navigation
134
452
  useInput((inputStr, key) => {
453
+ const approval = pendingApprovalRef.current;
454
+ if (approval) {
455
+ const resolveApproval = (approved) => {
456
+ pendingApprovalRef.current = null;
457
+ 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
+ }]);
465
+ approval.resolve(approved);
466
+ };
467
+
468
+ if (key.leftArrow || key.rightArrow || key.tab) {
469
+ const next = approvalChoiceRef.current === 'approve' ? 'deny' : 'approve';
470
+ approvalChoiceRef.current = next;
471
+ setApprovalChoice(next);
472
+ return;
473
+ }
474
+
475
+ const answer = String(inputStr || '').toLowerCase();
476
+ if (key.return) {
477
+ resolveApproval(approvalChoiceRef.current === 'approve');
478
+ return;
479
+ }
480
+ if (answer === 'y') {
481
+ resolveApproval(true);
482
+ return;
483
+ }
484
+ if (answer === 'n' || key.escape || (key.ctrl && inputStr === 'c')) {
485
+ resolveApproval(false);
486
+ return;
487
+ }
488
+ return;
489
+ }
490
+
491
+ if (key.escape && pendingImagesRef.current.length > 0) {
492
+ setPendingImages([]);
493
+ pendingImagesRef.current = [];
494
+ setPendingImagePrefix('');
495
+ pendingImagePrefixRef.current = '';
496
+ return;
497
+ }
498
+
499
+ if (key.escape && pendingPasteRef.current) {
500
+ setPendingPaste(null);
501
+ pendingPasteRef.current = null;
502
+ setPendingPastePrefix('');
503
+ pendingPastePrefixRef.current = '';
504
+ return;
505
+ }
506
+
507
+ 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
+ }
514
+ return;
515
+ }
516
+
135
517
  if (key.escape || (key.ctrl && inputStr === 'c')) {
136
- onExit();
137
518
  exit();
519
+ onExit();
520
+ return;
138
521
  }
139
522
 
140
523
  const currentInput = inputRef.current;
524
+ if (key.ctrl && inputStr === 'v') {
525
+ suppressPasteCharRef.current = true;
526
+ const inputBeforePaste = currentInput;
527
+ setInput(prev => removePasteArtifact(prev));
528
+ if (typeof onPasteImage === 'function') {
529
+ Promise.resolve(onPasteImage())
530
+ .then((image) => {
531
+ if (image) {
532
+ setPendingImages(prev => {
533
+ if (prev.length === 0) {
534
+ const prefix = normalizeInputText(inputBeforePaste).trim();
535
+ setPendingImagePrefix(prefix);
536
+ pendingImagePrefixRef.current = prefix;
537
+ }
538
+ return [...prev, image];
539
+ });
540
+ }
541
+ })
542
+ .catch((err) => {
543
+ setHistory(prev => [...prev, {
544
+ role: 'error',
545
+ text: err && err.message ? err.message : String(err || 'Unknown error'),
546
+ time: new Date()
547
+ }]);
548
+ })
549
+ .finally(() => {
550
+ setInput(prev => {
551
+ if (prev === `${inputBeforePaste}v` || prev === `${inputBeforePaste}V`) {
552
+ return inputBeforePaste;
553
+ }
554
+ return removePasteArtifact(prev);
555
+ });
556
+ });
557
+ }
558
+ return;
559
+ }
560
+
141
561
  const currentShowSuggestions = currentInput.startsWith('/') && !currentInput.includes(' ');
142
562
 
143
563
  if (currentShowSuggestions) {
@@ -160,10 +580,20 @@ async function createChatUI(options) {
160
580
  });
161
581
 
162
582
  const handleSubmit = (value) => {
163
- const text = value.trim();
164
- if (!text) return;
165
-
166
- if (showSuggestions && suggestions.length > 0) {
583
+ const text = normalizeInputText(value).trim();
584
+ const images = pendingImagesRef.current;
585
+ const imagePrefix = normalizeInputText(pendingImagePrefixRef.current).trim();
586
+ const imageLabels = images.map((_, index) => `[Image #${index + 1}]`).join(' ');
587
+ const pasted = pendingPasteRef.current;
588
+ const pastePrefix = normalizeInputText(pendingPastePrefixRef.current).trim();
589
+ const submittedText = pasted
590
+ ? [pastePrefix, pasted.text, text].filter(Boolean).join('\n\n')
591
+ : images.length > 0
592
+ ? [imagePrefix, imageLabels, text].filter(Boolean).join('\n\n')
593
+ : text;
594
+ if (!submittedText && images.length === 0) return;
595
+
596
+ if (!pasted && images.length === 0 && showSuggestions && suggestions.length > 0) {
167
597
  const picked = suggestions[selectedIndex];
168
598
  if (picked && text !== picked.cmd) {
169
599
  setInput(picked.cmd + ' ');
@@ -172,46 +602,97 @@ async function createChatUI(options) {
172
602
  }
173
603
 
174
604
  setInput('');
175
- onSubmit(text);
605
+ setPendingImages([]);
606
+ setPendingImagePrefix('');
607
+ setPendingPaste(null);
608
+ setPendingPastePrefix('');
609
+ pendingImagesRef.current = [];
610
+ pendingImagePrefixRef.current = '';
611
+ pendingPasteRef.current = null;
612
+ pendingPastePrefixRef.current = '';
613
+ onSubmit(submittedText, { images, pasted });
176
614
  };
177
615
 
178
- return h(Box, { flexDirection: 'column', paddingX: 1, width: '100%' },
179
- // Static History: Messages
180
- h(Static, { items: history }, (msg, index) => {
181
- if (msg.isThought) {
182
- return h(Box, { key: index, flexDirection: 'row', marginBottom: 0, paddingLeft: 2 },
183
- h(Text, { color: 'gray', dimColor: true }, `Thinking: ${msg.text}`)
184
- );
185
- }
616
+ const handleInputChange = (value) => {
617
+ if (shouldStoreAsPastedContent(value)) {
618
+ const normalized = normalizeInputText(value);
619
+ const previous = normalizeInputText(inputRef.current).trim();
620
+ setPendingPaste({ text: normalized, label: `[Pasted Content ${normalized.length} chars]` });
621
+ setPendingPastePrefix(previous);
622
+ setInput('');
623
+ return;
624
+ }
186
625
 
187
- let name = 'Mint';
188
- let nameColor = 'greenBright';
189
-
190
- if (msg.role === 'user') {
191
- name = 'You';
192
- nameColor = 'cyanBright';
193
- } else if (msg.role === 'error') {
194
- name = 'Error';
195
- nameColor = 'redBright';
196
- } else if (msg.role === 'system') {
197
- name = msg.label || 'System';
198
- nameColor = msg.labelColor || 'blueBright';
626
+ const normalizedValue = normalizeInputText(value);
627
+ if (suppressPasteCharRef.current) {
628
+ suppressPasteCharRef.current = false;
629
+ const previous = inputRef.current;
630
+ if (normalizedValue === `${previous}v` || normalizedValue === `${previous}V`) {
631
+ setInput(previous);
632
+ return;
633
+ }
634
+ if (normalizedValue.length > previous.length && /^[vV]$/.test(normalizedValue.slice(previous.length))) {
635
+ setInput(previous);
636
+ return;
199
637
  }
638
+ }
639
+ setInput(normalizedValue);
640
+ };
641
+
642
+ const renderMessage = (msg, index, keyPrefix = 'msg') => {
643
+ if (msg.isThought) {
644
+ return h(Box, { key: `${keyPrefix}-${index}`, flexDirection: 'row', marginBottom: 0, paddingLeft: 2 },
645
+ h(Text, { color: 'gray', dimColor: true }, `Thinking: ${msg.text}`)
646
+ );
647
+ }
200
648
 
201
- return h(Box, { key: index, flexDirection: 'column', marginBottom: 0 },
649
+ if (msg.isActivity) {
650
+ return h(Box, { key: `${keyPrefix}-${index}`, flexDirection: 'column', marginBottom: 0 },
202
651
  h(Box, null,
203
- h(Text, { bold: true, color: nameColor }, name),
204
- h(Text, { color: 'gray' }, ` ${msg.time instanceof Date ? msg.time.toLocaleTimeString() : ''}`)
652
+ h(Text, { color: 'greenBright' }, '• '),
653
+ h(Text, { bold: true, color: msg.labelColor || 'blueBright' }, msg.activityTitle || msg.label || 'Activity')
205
654
  ),
206
655
  h(Box, { paddingLeft: 2, marginBottom: 1 },
207
- h(Text, null, msg.text)
656
+ h(Text, { color: 'gray' }, '└ '),
657
+ h(Text, { color: 'cyanBright', wrap: 'wrap' }, msg.activityDetail || msg.text)
208
658
  )
209
659
  );
210
- }),
660
+ }
661
+
662
+ let name = 'Mint';
663
+ let nameColor = 'greenBright';
664
+
665
+ if (msg.role === 'user') {
666
+ name = 'You';
667
+ nameColor = 'cyanBright';
668
+ } else if (msg.role === 'error') {
669
+ name = 'Error';
670
+ nameColor = 'redBright';
671
+ } else if (msg.role === 'system') {
672
+ name = msg.label || 'System';
673
+ nameColor = msg.labelColor || 'blueBright';
674
+ }
675
+
676
+ return h(Box, { key: `${keyPrefix}-${index}`, flexDirection: 'column', marginBottom: 0 },
677
+ h(Box, null,
678
+ h(Text, { bold: true, color: nameColor }, name),
679
+ h(Text, { color: 'gray' }, ` ${msg.time instanceof Date ? msg.time.toLocaleTimeString() : ''}`)
680
+ ),
681
+ h(Box, { paddingLeft: 2, marginBottom: 1 },
682
+ h(Text, { wrap: 'wrap' }, cleanDisplayText(msg.text, msg.role))
683
+ )
684
+ );
685
+ };
686
+
687
+ return h(Box, { flexDirection: 'column', paddingX: 1, width: '100%' },
688
+ // Static History: Messages
689
+ h(Static, { items: history }, (msg, index) => renderMessage(msg, index, 'history')),
690
+ liveAssistant && renderMessage(liveAssistant, 'live', 'live'),
211
691
 
212
692
  // Floating (Persistent) UI part
213
693
  h(Box, { flexDirection: 'column' },
214
- thinking && h(Box, { marginBottom: 1 },
694
+ thinking && h(Box, { flexDirection: 'column', marginBottom: 1 },
695
+ h(Text, { color: 'gray', dimColor: true }, `─ Working for ${formatDuration(workingSeconds)} ─────────────────────────────────────────────────────────`),
215
696
  h(Text, { color: 'yellow' }, '● Mint is thinking...')
216
697
  ),
217
698
 
@@ -232,21 +713,64 @@ async function createChatUI(options) {
232
713
  ))
233
714
  ),
234
715
 
716
+ pendingApproval && h(Box, {
717
+ flexDirection: 'column',
718
+ borderStyle: 'single',
719
+ borderColor: 'yellow',
720
+ paddingX: 1,
721
+ marginBottom: 1
722
+ },
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
+ h(Box, null,
732
+ h(Text, {
733
+ color: approvalChoice === 'approve' ? 'black' : 'greenBright',
734
+ backgroundColor: approvalChoice === 'approve' ? 'greenBright' : undefined,
735
+ bold: true
736
+ }, ' Approve '),
737
+ h(Text, { color: 'gray' }, ' '),
738
+ h(Text, {
739
+ color: approvalChoice === 'deny' ? 'white' : 'redBright',
740
+ backgroundColor: approvalChoice === 'deny' ? 'redBright' : undefined,
741
+ bold: true
742
+ }, ' Deny '),
743
+ h(Text, { color: 'gray' }, ' Tab/←/→ Enter')
744
+ )
745
+ ),
746
+
235
747
  // Compact Input Area
236
- h(Box, { borderStyle: 'round', borderColor: 'greenBright', paddingX: 1, flexDirection: 'row' },
237
- h(Text, { bold: true, color: 'greenBright' }, '› '),
238
- h(TextInput, {
239
- value: input,
240
- onChange: setInput,
241
- onSubmit: handleSubmit,
242
- placeholder: 'Ask anything...'
243
- })
748
+ h(Box, { borderStyle: 'round', borderColor: pendingApproval ? 'gray' : 'greenBright', paddingX: 1, flexDirection: 'column' },
749
+ 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(' ') + ' '),
752
+ h(Text, { color: 'gray' }, 'Enter to send, Ctrl+Backspace remove, Esc clear')
753
+ ),
754
+ pendingPaste && h(Box, null,
755
+ pendingPastePrefix && h(Text, { color: 'cyanBright' }, '[Text before] '),
756
+ h(Text, { color: 'yellowBright' }, pendingPaste.label),
757
+ h(Text, { color: 'gray' }, ' Enter to send, Esc clear')
758
+ ),
759
+ h(Box, { flexDirection: 'row' },
760
+ h(Text, { bold: true, color: 'greenBright' }, '› '),
761
+ h(TextInput, {
762
+ value: input,
763
+ onChange: pendingApproval ? () => {} : handleInputChange,
764
+ onSubmit: pendingApproval ? () => {} : handleSubmit,
765
+ placeholder: pendingApproval ? 'Approval pending...' : 'Ask anything...'
766
+ })
767
+ )
244
768
  ),
245
769
 
246
770
  // Status Bar
247
771
  h(Box, { justifyContent: 'space-between' },
248
772
  h(Box, null,
249
- h(Text, { color: 'cyan' }, `[${mode}] `),
773
+ h(Text, { color: 'cyan' }, `[${fastMode ? 'Fast' : mode}] `),
250
774
  h(Text, { color: 'magentaBright' }, (model || config.geminiModel || 'gemini').slice(0, 46))
251
775
  ),
252
776
  h(Box, null,
@@ -265,30 +789,37 @@ async function createChatUI(options) {
265
789
  console.log(`\x1b[90mType naturally to chat. Esc to exit.\x1b[0m\n`);
266
790
 
267
791
  const ref = createRef();
268
- render(h(App, { ref, ...options }));
792
+ render(h(App, { ref, ...options }), { exitOnCtrlC: false });
269
793
 
270
794
  return {
271
795
  appendMessage: (role, text, metadata) => ref.current?.appendMessage(role, text, metadata),
272
- setThinking: (val) => ref.current?.setThinking(val),
796
+ setThinking: (val, seconds) => ref.current?.setThinking(val, seconds),
273
797
  setMode: (val) => ref.current?.setMode(val),
798
+ setFastMode: (val) => ref.current?.setFastMode(val),
799
+ toggleFastMode: () => ref.current?.toggleFastMode(),
800
+ getFastMode: () => ref.current?.getFastMode(),
801
+ setInputText: (val) => ref.current?.setInputText(val),
802
+ setPendingPasteText: (text) => ref.current?.setPendingPasteText(text),
274
803
  updateStatusModel: (val) => ref.current?.updateStatusModel(val),
275
804
  updateWorkspace: (val) => ref.current?.updateWorkspace(val),
805
+ attachImage: (image) => ref.current?.attachImage(image),
276
806
  appendCodeStep: (info) => ref.current?.appendCodeStep(info),
277
- streamMessage: () => {
278
- let fullText = '';
807
+ streamMessage: (metadata = {}) => {
808
+ ref.current?.beginAssistantStream(metadata);
279
809
  return {
280
810
  appendChunk: (chunk) => {
281
- fullText += chunk;
811
+ if (!String(chunk || '').trim()) return;
812
+ ref.current?.appendAssistantStreamChunk(chunk);
282
813
  },
283
814
  finalize: () => {
284
- ref.current?.appendMessage('assistant', fullText);
815
+ ref.current?.finalizeAssistantStream();
285
816
  }
286
817
  };
287
818
  },
288
819
  copyLastResponse: () => false,
289
- requestApproval: () => Promise.resolve(true),
820
+ requestApproval: (request) => ref.current?.requestApproval(request) || Promise.resolve(false),
290
821
  askUser: () => Promise.resolve('')
291
822
  };
292
823
  }
293
824
 
294
- module.exports = { createChatUI };
825
+ module.exports = { createChatUI, _helpers: { cleanDisplayText, stripInlineMarkdown, compactPathLabel, formatActivityStep, formatDuration, shouldAppendMessage } };