@pheem49/mint 1.5.0 → 1.5.1

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 (78) hide show
  1. package/README.md +27 -1
  2. package/main.js +28 -14
  3. package/mint-cli-logic.js +3 -119
  4. package/mint-cli.js +497 -23
  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 +10 -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 +10 -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 +26 -1
  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/CLI/chat_router.js +18 -6
  30. package/src/CLI/chat_ui.js +396 -50
  31. package/src/CLI/code_agent.js +203 -14
  32. package/src/CLI/image_input.js +90 -0
  33. package/src/CLI/onboarding.js +72 -15
  34. package/src/CLI/updater.js +6 -4
  35. package/src/System/action_executor.js +59 -10
  36. package/src/System/config_manager.js +31 -1
  37. package/src/System/granular_automation.js +122 -53
  38. package/src/System/proactive_loop.js +19 -3
  39. package/src/System/safety_manager.js +108 -0
  40. package/src/System/sandbox_runner.js +182 -0
  41. package/src/System/system_automation.js +127 -81
  42. package/src/System/system_info.js +70 -0
  43. package/src/System/tool_registry.js +280 -0
  44. package/src/System/window_manager.js +4 -2
  45. package/src/UI/live2d_manager.js +368 -0
  46. package/src/UI/renderer.js +176 -18
  47. package/src/UI/styles.css +452 -31
  48. package/.codex +0 -0
  49. package/docs/assets/Agent_Mint.png +0 -0
  50. package/docs/assets/CLI_Screen.png +0 -0
  51. package/docs/assets/Settings.png +0 -0
  52. package/docs/assets/icon.png +0 -0
  53. package/docs/guide.html +0 -632
  54. package/docs/index.html +0 -133
  55. package/docs/style.css +0 -579
  56. package/index.html +0 -16
  57. package/src/UI/index.html +0 -126
  58. package/tech_news.txt +0 -3
  59. package/test_knowledge.txt +0 -3
  60. package/tests/action_executor_safety.test.js +0 -67
  61. package/tests/agent_orchestrator.test.js +0 -41
  62. package/tests/chat_router.test.js +0 -42
  63. package/tests/code_agent.test.js +0 -69
  64. package/tests/config_manager.test.js +0 -141
  65. package/tests/docker.test.js +0 -46
  66. package/tests/file_operations.test.js +0 -57
  67. package/tests/gmail.test.js +0 -135
  68. package/tests/gmail_auth.test.js +0 -129
  69. package/tests/google_calendar.test.js +0 -113
  70. package/tests/google_tts_urls.test.js +0 -24
  71. package/tests/memory_store.test.js +0 -185
  72. package/tests/notion.test.js +0 -121
  73. package/tests/provider_routing.test.js +0 -83
  74. package/tests/safety_manager.test.js +0 -40
  75. package/tests/spotify.test.js +0 -201
  76. package/tests/system_monitor.test.js +0 -37
  77. package/tests/updater.test.js +0 -32
  78. 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' },
@@ -35,29 +41,96 @@ async function createChatUI(options) {
35
41
  const TextInput = (await import('ink-text-input')).default;
36
42
  const { useState, useImperativeHandle, forwardRef, createRef, useEffect, useMemo } = React;
37
43
 
38
- const App = forwardRef(({ onSubmit, onExit, initialHistory = [] }, ref) => {
44
+ const App = forwardRef(({ onSubmit, onExit, onPasteImage, initialHistory = [] }, ref) => {
39
45
  const config = readConfig();
40
46
  const { exit } = useApp();
41
47
  const [input, setInput] = useState('');
42
48
  const [history, setHistory] = useState(initialHistory);
49
+ const [liveAssistant, setLiveAssistant] = useState(null);
43
50
  const [thinking, setThinking] = useState(false);
44
- const [mode, setMode] = useState('Chat');
51
+ const [fastMode, setFastMode] = useState(false);
52
+ const [mode, setMode] = useState('Agent');
45
53
  const [model, setModel] = useState('');
46
54
  const [workspace, setWorkspace] = useState(process.cwd());
55
+ const [pendingImages, setPendingImages] = useState([]);
56
+ const [pendingImagePrefix, setPendingImagePrefix] = useState('');
57
+ const [pendingPaste, setPendingPaste] = useState(null);
58
+ const [pendingPastePrefix, setPendingPastePrefix] = useState('');
59
+ const [pendingApproval, setPendingApproval] = useState(null);
60
+ const [approvalChoice, setApprovalChoice] = useState('approve');
47
61
 
48
62
  // Suggestions State
49
63
  const [selectedIndex, setSelectedIndex] = useState(0);
50
64
  const inputRef = React.useRef(input);
65
+ const pendingImagesRef = React.useRef(pendingImages);
66
+ const pendingImagePrefixRef = React.useRef(pendingImagePrefix);
67
+ const pendingPasteRef = React.useRef(pendingPaste);
68
+ const pendingPastePrefixRef = React.useRef(pendingPastePrefix);
69
+ const liveAssistantRef = React.useRef(liveAssistant);
70
+ const fastModeRef = React.useRef(fastMode);
71
+ const suppressPasteCharRef = React.useRef(false);
51
72
  const selectedIndexRef = React.useRef(selectedIndex);
73
+ const pendingApprovalRef = React.useRef(null);
74
+ const approvalChoiceRef = React.useRef('approve');
75
+
76
+ const removePasteArtifact = (value) => {
77
+ const text = String(value || '');
78
+ return text.replace(/[vV]$/, '');
79
+ };
80
+
81
+ const normalizeInputText = (value) => {
82
+ return String(value || '').replace(/\s*[\r\n]+\s*/g, ' ');
83
+ };
84
+
85
+ const shouldStoreAsPastedContent = (value) => {
86
+ const text = String(value || '');
87
+ return text.length > 500 || /[\r\n]/.test(text);
88
+ };
52
89
 
53
90
  useEffect(() => {
54
91
  inputRef.current = input;
55
92
  }, [input]);
56
93
 
94
+ useEffect(() => {
95
+ pendingImagesRef.current = pendingImages;
96
+ }, [pendingImages]);
97
+
98
+ useEffect(() => {
99
+ pendingImagePrefixRef.current = pendingImagePrefix;
100
+ }, [pendingImagePrefix]);
101
+
102
+ useEffect(() => {
103
+ pendingPasteRef.current = pendingPaste;
104
+ }, [pendingPaste]);
105
+
106
+ useEffect(() => {
107
+ pendingPastePrefixRef.current = pendingPastePrefix;
108
+ }, [pendingPastePrefix]);
109
+
110
+ useEffect(() => {
111
+ liveAssistantRef.current = liveAssistant;
112
+ }, [liveAssistant]);
113
+
114
+ useEffect(() => {
115
+ fastModeRef.current = fastMode;
116
+ }, [fastMode]);
117
+
57
118
  useEffect(() => {
58
119
  selectedIndexRef.current = selectedIndex;
59
120
  }, [selectedIndex]);
60
121
 
122
+ useEffect(() => {
123
+ pendingApprovalRef.current = pendingApproval;
124
+ if (pendingApproval) {
125
+ approvalChoiceRef.current = 'approve';
126
+ setApprovalChoice('approve');
127
+ }
128
+ }, [pendingApproval]);
129
+
130
+ useEffect(() => {
131
+ approvalChoiceRef.current = approvalChoice;
132
+ }, [approvalChoice]);
133
+
61
134
  const showSuggestions = input.startsWith('/') && !input.includes(' ');
62
135
  const suggestions = useMemo(() => {
63
136
  if (!showSuggestions) return [];
@@ -81,11 +154,69 @@ async function createChatUI(options) {
81
154
  setModel(model ? `${provider} • ${model}` : provider);
82
155
  }
83
156
  },
157
+ beginAssistantStream: (metadata = {}) => {
158
+ const msg = { role: 'assistant', text: '', time: new Date(), ...metadata };
159
+ liveAssistantRef.current = msg;
160
+ setLiveAssistant(msg);
161
+ if (metadata.providerInfo) {
162
+ const { provider, model } = metadata.providerInfo;
163
+ setModel(model ? `${provider} • ${model}` : provider);
164
+ }
165
+ },
166
+ appendAssistantStreamChunk: (chunk) => {
167
+ const current = liveAssistantRef.current || { role: 'assistant', text: '', time: new Date() };
168
+ const next = { ...current, text: `${current.text || ''}${chunk}` };
169
+ liveAssistantRef.current = next;
170
+ setLiveAssistant(next);
171
+ },
172
+ finalizeAssistantStream: () => {
173
+ const current = liveAssistantRef.current;
174
+ liveAssistantRef.current = null;
175
+ setLiveAssistant(null);
176
+ if (current && String(current.text || '').trim()) {
177
+ setHistory(prev => [...prev, current]);
178
+ }
179
+ },
84
180
  setThinking: (val) => setThinking(val),
85
181
  setMode: (val) => setMode(val),
182
+ setFastMode: (val) => {
183
+ const next = Boolean(val);
184
+ fastModeRef.current = next;
185
+ setFastMode(next);
186
+ return next;
187
+ },
188
+ toggleFastMode: () => {
189
+ const next = !fastModeRef.current;
190
+ fastModeRef.current = next;
191
+ setFastMode(next);
192
+ return next;
193
+ },
194
+ getFastMode: () => fastModeRef.current,
195
+ setInputText: (val) => setInput(val || ''),
196
+ setPendingPasteText: (text) => {
197
+ const normalized = normalizeInputText(text);
198
+ setPendingPaste({ text: normalized, label: `[Pasted Content ${normalized.length} chars]` });
199
+ setPendingPastePrefix('');
200
+ setInput('');
201
+ },
86
202
  updateStatusModel: (val) => setModel(val),
87
203
  updateWorkspace: (val) => setWorkspace(val),
204
+ attachImage: (image) => {
205
+ setPendingImages(prev => {
206
+ if (prev.length === 0) {
207
+ const prefix = normalizeInputText(inputRef.current).trim();
208
+ setPendingImagePrefix(prefix);
209
+ pendingImagePrefixRef.current = prefix;
210
+ setInput('');
211
+ }
212
+ return [...prev, image];
213
+ });
214
+ },
88
215
  appendCodeStep: (info) => {
216
+ if (fastModeRef.current) {
217
+ return;
218
+ }
219
+
89
220
  let text = '';
90
221
  let label = 'System';
91
222
  let labelColor = 'blueBright';
@@ -95,6 +226,9 @@ async function createChatUI(options) {
95
226
  text = info;
96
227
  } else {
97
228
  const { action, phase, target, message, thought } = info;
229
+ if (action === 'memory_context' && process.env.MINT_SHOW_MEMORY_TRACE !== '1') {
230
+ return;
231
+ }
98
232
  if (thought) {
99
233
  text = thought;
100
234
  label = 'Thinking';
@@ -127,17 +261,131 @@ async function createChatUI(options) {
127
261
  isThought,
128
262
  time: new Date()
129
263
  }]);
264
+ },
265
+ requestApproval: (request = {}) => {
266
+ return new Promise((resolve) => {
267
+ const approval = {
268
+ type: request.type || 'action',
269
+ label: request.label || 'Requested action',
270
+ preview: request.preview || '',
271
+ resolve
272
+ };
273
+ pendingApprovalRef.current = approval;
274
+ setPendingApproval(approval);
275
+ });
130
276
  }
131
277
  }));
132
278
 
133
279
  // Handle exiting and keyboard navigation
134
280
  useInput((inputStr, key) => {
281
+ const approval = pendingApprovalRef.current;
282
+ if (approval) {
283
+ const resolveApproval = (approved) => {
284
+ pendingApprovalRef.current = null;
285
+ 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
+ }]);
293
+ approval.resolve(approved);
294
+ };
295
+
296
+ if (key.leftArrow || key.rightArrow || key.tab) {
297
+ const next = approvalChoiceRef.current === 'approve' ? 'deny' : 'approve';
298
+ approvalChoiceRef.current = next;
299
+ setApprovalChoice(next);
300
+ return;
301
+ }
302
+
303
+ const answer = String(inputStr || '').toLowerCase();
304
+ if (key.return) {
305
+ resolveApproval(approvalChoiceRef.current === 'approve');
306
+ return;
307
+ }
308
+ if (answer === 'y') {
309
+ resolveApproval(true);
310
+ return;
311
+ }
312
+ if (answer === 'n' || key.escape || (key.ctrl && inputStr === 'c')) {
313
+ resolveApproval(false);
314
+ return;
315
+ }
316
+ return;
317
+ }
318
+
319
+ if (key.escape && pendingImagesRef.current.length > 0) {
320
+ setPendingImages([]);
321
+ pendingImagesRef.current = [];
322
+ setPendingImagePrefix('');
323
+ pendingImagePrefixRef.current = '';
324
+ return;
325
+ }
326
+
327
+ if (key.escape && pendingPasteRef.current) {
328
+ setPendingPaste(null);
329
+ pendingPasteRef.current = null;
330
+ setPendingPastePrefix('');
331
+ pendingPastePrefixRef.current = '';
332
+ return;
333
+ }
334
+
335
+ 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
+ }
342
+ return;
343
+ }
344
+
135
345
  if (key.escape || (key.ctrl && inputStr === 'c')) {
136
- onExit();
137
346
  exit();
347
+ onExit();
348
+ return;
138
349
  }
139
350
 
140
351
  const currentInput = inputRef.current;
352
+ if (key.ctrl && inputStr === 'v') {
353
+ suppressPasteCharRef.current = true;
354
+ const inputBeforePaste = currentInput;
355
+ setInput(prev => removePasteArtifact(prev));
356
+ if (typeof onPasteImage === 'function') {
357
+ Promise.resolve(onPasteImage())
358
+ .then((image) => {
359
+ if (image) {
360
+ setPendingImages(prev => {
361
+ if (prev.length === 0) {
362
+ const prefix = normalizeInputText(inputBeforePaste).trim();
363
+ setPendingImagePrefix(prefix);
364
+ pendingImagePrefixRef.current = prefix;
365
+ }
366
+ return [...prev, image];
367
+ });
368
+ }
369
+ })
370
+ .catch((err) => {
371
+ setHistory(prev => [...prev, {
372
+ role: 'error',
373
+ text: err && err.message ? err.message : String(err || 'Unknown error'),
374
+ time: new Date()
375
+ }]);
376
+ })
377
+ .finally(() => {
378
+ setInput(prev => {
379
+ if (prev === `${inputBeforePaste}v` || prev === `${inputBeforePaste}V`) {
380
+ return inputBeforePaste;
381
+ }
382
+ return removePasteArtifact(prev);
383
+ });
384
+ });
385
+ }
386
+ return;
387
+ }
388
+
141
389
  const currentShowSuggestions = currentInput.startsWith('/') && !currentInput.includes(' ');
142
390
 
143
391
  if (currentShowSuggestions) {
@@ -160,10 +408,20 @@ async function createChatUI(options) {
160
408
  });
161
409
 
162
410
  const handleSubmit = (value) => {
163
- const text = value.trim();
164
- if (!text) return;
165
-
166
- if (showSuggestions && suggestions.length > 0) {
411
+ const text = normalizeInputText(value).trim();
412
+ const images = pendingImagesRef.current;
413
+ const imagePrefix = normalizeInputText(pendingImagePrefixRef.current).trim();
414
+ const imageLabels = images.map((_, index) => `[Image #${index + 1}]`).join(' ');
415
+ const pasted = pendingPasteRef.current;
416
+ const pastePrefix = normalizeInputText(pendingPastePrefixRef.current).trim();
417
+ const submittedText = pasted
418
+ ? [pastePrefix, pasted.text, text].filter(Boolean).join('\n\n')
419
+ : images.length > 0
420
+ ? [imagePrefix, imageLabels, text].filter(Boolean).join('\n\n')
421
+ : text;
422
+ if (!submittedText && images.length === 0) return;
423
+
424
+ if (!pasted && images.length === 0 && showSuggestions && suggestions.length > 0) {
167
425
  const picked = suggestions[selectedIndex];
168
426
  if (picked && text !== picked.cmd) {
169
427
  setInput(picked.cmd + ' ');
@@ -172,42 +430,79 @@ async function createChatUI(options) {
172
430
  }
173
431
 
174
432
  setInput('');
175
- onSubmit(text);
433
+ setPendingImages([]);
434
+ setPendingImagePrefix('');
435
+ setPendingPaste(null);
436
+ setPendingPastePrefix('');
437
+ pendingImagesRef.current = [];
438
+ pendingImagePrefixRef.current = '';
439
+ pendingPasteRef.current = null;
440
+ pendingPastePrefixRef.current = '';
441
+ onSubmit(submittedText, { images, pasted });
176
442
  };
177
443
 
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
- }
444
+ const handleInputChange = (value) => {
445
+ if (shouldStoreAsPastedContent(value)) {
446
+ const normalized = normalizeInputText(value);
447
+ const previous = normalizeInputText(inputRef.current).trim();
448
+ setPendingPaste({ text: normalized, label: `[Pasted Content ${normalized.length} chars]` });
449
+ setPendingPastePrefix(previous);
450
+ setInput('');
451
+ return;
452
+ }
186
453
 
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';
454
+ const normalizedValue = normalizeInputText(value);
455
+ if (suppressPasteCharRef.current) {
456
+ suppressPasteCharRef.current = false;
457
+ const previous = inputRef.current;
458
+ if (normalizedValue === `${previous}v` || normalizedValue === `${previous}V`) {
459
+ setInput(previous);
460
+ return;
461
+ }
462
+ if (normalizedValue.length > previous.length && /^[vV]$/.test(normalizedValue.slice(previous.length))) {
463
+ setInput(previous);
464
+ return;
199
465
  }
466
+ }
467
+ setInput(normalizedValue);
468
+ };
200
469
 
201
- return h(Box, { key: index, flexDirection: 'column', marginBottom: 0 },
202
- h(Box, null,
203
- h(Text, { bold: true, color: nameColor }, name),
204
- h(Text, { color: 'gray' }, ` ${msg.time instanceof Date ? msg.time.toLocaleTimeString() : ''}`)
205
- ),
206
- h(Box, { paddingLeft: 2, marginBottom: 1 },
207
- h(Text, null, msg.text)
208
- )
470
+ const renderMessage = (msg, index, keyPrefix = 'msg') => {
471
+ if (msg.isThought) {
472
+ return h(Box, { key: `${keyPrefix}-${index}`, flexDirection: 'row', marginBottom: 0, paddingLeft: 2 },
473
+ h(Text, { color: 'gray', dimColor: true }, `Thinking: ${msg.text}`)
209
474
  );
210
- }),
475
+ }
476
+
477
+ let name = 'Mint';
478
+ let nameColor = 'greenBright';
479
+
480
+ if (msg.role === 'user') {
481
+ name = 'You';
482
+ nameColor = 'cyanBright';
483
+ } else if (msg.role === 'error') {
484
+ name = 'Error';
485
+ nameColor = 'redBright';
486
+ } else if (msg.role === 'system') {
487
+ name = msg.label || 'System';
488
+ nameColor = msg.labelColor || 'blueBright';
489
+ }
490
+
491
+ return h(Box, { key: `${keyPrefix}-${index}`, flexDirection: 'column', marginBottom: 0 },
492
+ h(Box, null,
493
+ h(Text, { bold: true, color: nameColor }, name),
494
+ h(Text, { color: 'gray' }, ` ${msg.time instanceof Date ? msg.time.toLocaleTimeString() : ''}`)
495
+ ),
496
+ h(Box, { paddingLeft: 2, marginBottom: 1 },
497
+ h(Text, null, msg.text)
498
+ )
499
+ );
500
+ };
501
+
502
+ return h(Box, { flexDirection: 'column', paddingX: 1, width: '100%' },
503
+ // Static History: Messages
504
+ h(Static, { items: history }, (msg, index) => renderMessage(msg, index, 'history')),
505
+ liveAssistant && renderMessage(liveAssistant, 'live', 'live'),
211
506
 
212
507
  // Floating (Persistent) UI part
213
508
  h(Box, { flexDirection: 'column' },
@@ -232,21 +527,64 @@ async function createChatUI(options) {
232
527
  ))
233
528
  ),
234
529
 
530
+ pendingApproval && h(Box, {
531
+ flexDirection: 'column',
532
+ borderStyle: 'single',
533
+ borderColor: 'yellow',
534
+ paddingX: 1,
535
+ marginBottom: 1
536
+ },
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
+ h(Box, null,
546
+ h(Text, {
547
+ color: approvalChoice === 'approve' ? 'black' : 'greenBright',
548
+ backgroundColor: approvalChoice === 'approve' ? 'greenBright' : undefined,
549
+ bold: true
550
+ }, ' Approve '),
551
+ h(Text, { color: 'gray' }, ' '),
552
+ h(Text, {
553
+ color: approvalChoice === 'deny' ? 'white' : 'redBright',
554
+ backgroundColor: approvalChoice === 'deny' ? 'redBright' : undefined,
555
+ bold: true
556
+ }, ' Deny '),
557
+ h(Text, { color: 'gray' }, ' Tab/←/→ Enter')
558
+ )
559
+ ),
560
+
235
561
  // 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
- })
562
+ h(Box, { borderStyle: 'round', borderColor: pendingApproval ? 'gray' : 'greenBright', paddingX: 1, flexDirection: 'column' },
563
+ 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(' ') + ' '),
566
+ h(Text, { color: 'gray' }, 'Enter to send, Ctrl+Backspace remove, Esc clear')
567
+ ),
568
+ pendingPaste && h(Box, null,
569
+ pendingPastePrefix && h(Text, { color: 'cyanBright' }, '[Text before] '),
570
+ h(Text, { color: 'yellowBright' }, pendingPaste.label),
571
+ h(Text, { color: 'gray' }, ' Enter to send, Esc clear')
572
+ ),
573
+ h(Box, { flexDirection: 'row' },
574
+ h(Text, { bold: true, color: 'greenBright' }, '› '),
575
+ h(TextInput, {
576
+ value: input,
577
+ onChange: pendingApproval ? () => {} : handleInputChange,
578
+ onSubmit: pendingApproval ? () => {} : handleSubmit,
579
+ placeholder: pendingApproval ? 'Approval pending...' : 'Ask anything...'
580
+ })
581
+ )
244
582
  ),
245
583
 
246
584
  // Status Bar
247
585
  h(Box, { justifyContent: 'space-between' },
248
586
  h(Box, null,
249
- h(Text, { color: 'cyan' }, `[${mode}] `),
587
+ h(Text, { color: 'cyan' }, `[${fastMode ? 'Fast' : mode}] `),
250
588
  h(Text, { color: 'magentaBright' }, (model || config.geminiModel || 'gemini').slice(0, 46))
251
589
  ),
252
590
  h(Box, null,
@@ -265,28 +603,36 @@ async function createChatUI(options) {
265
603
  console.log(`\x1b[90mType naturally to chat. Esc to exit.\x1b[0m\n`);
266
604
 
267
605
  const ref = createRef();
268
- render(h(App, { ref, ...options }));
606
+ render(h(App, { ref, ...options }), { exitOnCtrlC: false });
269
607
 
270
608
  return {
271
609
  appendMessage: (role, text, metadata) => ref.current?.appendMessage(role, text, metadata),
272
610
  setThinking: (val) => ref.current?.setThinking(val),
273
611
  setMode: (val) => ref.current?.setMode(val),
612
+ setFastMode: (val) => ref.current?.setFastMode(val),
613
+ toggleFastMode: () => ref.current?.toggleFastMode(),
614
+ getFastMode: () => ref.current?.getFastMode(),
615
+ setInputText: (val) => ref.current?.setInputText(val),
616
+ setPendingPasteText: (text) => ref.current?.setPendingPasteText(text),
274
617
  updateStatusModel: (val) => ref.current?.updateStatusModel(val),
275
618
  updateWorkspace: (val) => ref.current?.updateWorkspace(val),
619
+ attachImage: (image) => ref.current?.attachImage(image),
276
620
  appendCodeStep: (info) => ref.current?.appendCodeStep(info),
277
- streamMessage: () => {
621
+ streamMessage: (metadata = {}) => {
278
622
  let fullText = '';
623
+ ref.current?.beginAssistantStream(metadata);
279
624
  return {
280
625
  appendChunk: (chunk) => {
281
626
  fullText += chunk;
627
+ ref.current?.appendAssistantStreamChunk(chunk);
282
628
  },
283
629
  finalize: () => {
284
- ref.current?.appendMessage('assistant', fullText);
630
+ ref.current?.finalizeAssistantStream();
285
631
  }
286
632
  };
287
633
  },
288
634
  copyLastResponse: () => false,
289
- requestApproval: () => Promise.resolve(true),
635
+ requestApproval: (request) => ref.current?.requestApproval(request) || Promise.resolve(false),
290
636
  askUser: () => Promise.resolve('')
291
637
  };
292
638
  }