@pheem49/mint 1.4.2 → 1.5.0
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.
- package/GUIDE_TH.md +113 -0
- package/README.md +239 -76
- package/assets/CLI_Screen.png +0 -0
- package/docs/assets/CLI_Screen.png +0 -0
- package/docs/guide.html +632 -0
- package/docs/index.html +5 -4
- package/main.js +66 -894
- package/mint-cli-logic.js +13 -1
- package/mint-cli.js +100 -9
- package/package.json +12 -4
- package/src/AI_Brain/Gemini_API.js +77 -20
- package/src/AI_Brain/autonomous_brain.js +10 -0
- package/src/AI_Brain/behavior_memory.js +26 -5
- package/src/AI_Brain/headless_agent.js +4 -0
- package/src/AI_Brain/knowledge_base.js +61 -8
- package/src/AI_Brain/memory_store.js +55 -7
- package/src/Automation_Layer/file_operations.js +1 -1
- package/src/CLI/chat_router.js +3 -2
- package/src/CLI/chat_ui.js +263 -838
- package/src/CLI/code_agent.js +144 -42
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/list_features.js +2 -0
- package/src/CLI/onboarding.js +307 -55
- package/src/CLI/updater.js +208 -0
- package/src/Channels/brave_search_bridge.js +35 -0
- package/src/Channels/discord_bridge.js +68 -0
- package/src/Channels/google_search_bridge.js +38 -0
- package/src/Channels/line_bridge.js +60 -0
- package/src/Channels/slack_bridge.js +53 -0
- package/src/Channels/telegram_bridge.js +49 -0
- package/src/Channels/whatsapp_bridge.js +55 -0
- package/src/Command_Parser/parser.js +12 -1
- package/src/Plugins/gmail.js +251 -0
- package/src/Plugins/google_calendar.js +245 -19
- package/src/Plugins/notion.js +256 -0
- package/src/System/action_executor.js +129 -0
- package/src/System/bridge_manager.js +76 -0
- package/src/System/chat_history_manager.js +23 -5
- package/src/System/config_manager.js +41 -7
- package/src/System/custom_workflows.js +31 -2
- package/src/System/google_tts_urls.js +51 -0
- package/src/System/ipc_handlers.js +238 -0
- package/src/System/proactive_loop.js +137 -0
- package/src/System/safety_manager.js +165 -0
- package/src/System/screen_capture.js +175 -0
- package/src/System/task_manager.js +15 -5
- package/src/System/window_manager.js +210 -0
- package/src/UI/renderer.js +33 -7
- package/src/UI/settings.html +24 -0
- package/src/UI/settings.js +14 -4
- package/src/UI/styles.css +14 -1
- package/tests/action_executor_safety.test.js +67 -0
- package/tests/gmail.test.js +135 -0
- package/tests/gmail_auth.test.js +129 -0
- package/tests/google_calendar.test.js +113 -0
- package/tests/google_tts_urls.test.js +24 -0
- package/tests/notion.test.js +121 -0
- package/tests/provider_routing.test.js +17 -1
- package/tests/safety_manager.test.js +40 -0
- package/tests/updater.test.js +32 -0
package/src/CLI/chat_ui.js
CHANGED
|
@@ -1,869 +1,294 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mint CLI -
|
|
3
|
-
*
|
|
2
|
+
* Mint CLI - Ink-based UI (ESM-compatible Version)
|
|
3
|
+
* A modern, React-based terminal UI for a better chat experience.
|
|
4
|
+
* Uses dynamic imports to handle ESM dependencies (Ink).
|
|
4
5
|
*/
|
|
5
|
-
const
|
|
6
|
+
const React = require('react');
|
|
6
7
|
const path = require('path');
|
|
7
|
-
const { execSync } = require('child_process');
|
|
8
8
|
const { readConfig } = require('../System/config_manager');
|
|
9
9
|
|
|
10
|
+
// Helper to make element creation less verbose
|
|
11
|
+
const h = React.createElement;
|
|
12
|
+
|
|
10
13
|
const SLASH_COMMANDS = [
|
|
11
|
-
{
|
|
12
|
-
{
|
|
13
|
-
{
|
|
14
|
-
{
|
|
15
|
-
{
|
|
16
|
-
{
|
|
17
|
-
{
|
|
18
|
-
{
|
|
19
|
-
{
|
|
20
|
-
{
|
|
21
|
-
{
|
|
22
|
-
{
|
|
14
|
+
{ cmd: '/help', desc: 'Show available commands' },
|
|
15
|
+
{ cmd: '/code', desc: 'Force workspace Code Mode' },
|
|
16
|
+
{ cmd: '/cd', desc: 'Change current working directory' },
|
|
17
|
+
{ cmd: '/models', desc: 'List or switch Gemini models' },
|
|
18
|
+
{ cmd: '/config', desc: 'Show current configuration' },
|
|
19
|
+
{ cmd: '/copy', desc: 'Copy last response to clipboard' },
|
|
20
|
+
{ cmd: '/clear', desc: 'Clear conversation history' },
|
|
21
|
+
{ cmd: '/reset', desc: 'Reset conversation history' },
|
|
22
|
+
{ cmd: '/agent', desc: 'Switch AI agents (e.g. /agent code)' },
|
|
23
|
+
{ cmd: '/workspace', desc: 'Manage registered workspaces' },
|
|
24
|
+
{ cmd: '/stats', desc: 'Show system statistics' },
|
|
25
|
+
{ cmd: '/review', desc: 'Request second-pass review' },
|
|
26
|
+
{ cmd: '/exit', desc: 'Exit Mint' }
|
|
23
27
|
];
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
|
-
*
|
|
27
|
-
* @param {Object} options
|
|
28
|
-
* @param {Function} options.onSubmit - Called with (userInput: string) when user sends a message
|
|
29
|
-
* @param {Function} options.onExit - Called when user exits
|
|
30
|
-
* @returns {{ screen, appendMessage, setThinking }}
|
|
30
|
+
* We wrap everything in an async function to load ESM modules
|
|
31
31
|
*/
|
|
32
|
-
function createChatUI(
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
label: ' Conversation '
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// ─── Hint bar ─────────────────────────────────────────────────────────────
|
|
86
|
-
const hintBar = blessed.box({
|
|
87
|
-
bottom: 6, left: 1, width: '100%-2', height: 1,
|
|
88
|
-
tags: true,
|
|
89
|
-
content: HINT_DEFAULT,
|
|
90
|
-
style: { bg: 'default' }
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const inputBox = blessed.textbox({
|
|
94
|
-
bottom: 3, left: 1, width: '100%-2', height: 3,
|
|
95
|
-
tags: false,
|
|
96
|
-
inputOnFocus: false, // We'll manage this manually for stability
|
|
97
|
-
keys: true,
|
|
98
|
-
style: {
|
|
99
|
-
bg: INPUT_BG,
|
|
100
|
-
fg: INPUT_FG,
|
|
101
|
-
border: { fg: '#335d52' },
|
|
102
|
-
focus: {
|
|
103
|
-
fg: INPUT_FG,
|
|
104
|
-
bg: INPUT_BG,
|
|
105
|
-
border: { fg: '#88e0b0' }
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
border: { type: 'line' },
|
|
109
|
-
padding: { left: 1 },
|
|
110
|
-
label: ' Message '
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// --- SAFETY PATCH ---
|
|
114
|
-
// Prevent "TypeError: done is not a function" if a listener survives a blur/focus cycle.
|
|
115
|
-
const originalListener = inputBox._listener;
|
|
116
|
-
inputBox._listener = function(ch, key) {
|
|
117
|
-
if (typeof this._done !== 'function') return;
|
|
118
|
-
return originalListener.call(this, ch, key);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// ─── Placeholder (SIBLING widget floating over input content area) ─────────
|
|
124
|
-
// inputBox: bottom=3, height=3, border=1 → content row at bottom=4, left=2
|
|
125
|
-
const placeholderWidget = blessed.text({
|
|
126
|
-
bottom: 4, // inside input content area (border offset)
|
|
127
|
-
left: 3,
|
|
128
|
-
width: '100%-6',
|
|
129
|
-
height: 1,
|
|
130
|
-
content: '> Ask anything, or describe a coding task for this workspace',
|
|
131
|
-
tags: false,
|
|
132
|
-
style: { fg: '#5d6678', bg: '#10141c' }
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
let placeholderVisible = true;
|
|
136
|
-
|
|
137
|
-
function hidePlaceholder() {
|
|
138
|
-
if (placeholderVisible) {
|
|
139
|
-
placeholderVisible = false;
|
|
140
|
-
placeholderWidget.hide();
|
|
141
|
-
screen.render();
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function showPlaceholder() {
|
|
146
|
-
if (!placeholderVisible) {
|
|
147
|
-
placeholderVisible = true;
|
|
148
|
-
placeholderWidget.show();
|
|
149
|
-
screen.render();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function refreshInputStyles() {
|
|
154
|
-
inputBox.style.fg = INPUT_FG;
|
|
155
|
-
inputBox.style.bg = INPUT_BG;
|
|
156
|
-
if (inputBox.style.focus) {
|
|
157
|
-
inputBox.style.focus.fg = INPUT_FG;
|
|
158
|
-
inputBox.style.focus.bg = INPUT_BG;
|
|
159
|
-
}
|
|
160
|
-
if (Array.isArray(inputBox.children)) {
|
|
161
|
-
inputBox.children.forEach((child) => {
|
|
162
|
-
if (child.style) {
|
|
163
|
-
child.style.fg = INPUT_FG;
|
|
164
|
-
child.style.bg = INPUT_BG;
|
|
32
|
+
async function createChatUI(options) {
|
|
33
|
+
// Dynamic imports for ESM modules
|
|
34
|
+
const { render, Box, Text, useInput, useApp, Static } = await import('ink');
|
|
35
|
+
const TextInput = (await import('ink-text-input')).default;
|
|
36
|
+
const { useState, useImperativeHandle, forwardRef, createRef, useEffect, useMemo } = React;
|
|
37
|
+
|
|
38
|
+
const App = forwardRef(({ onSubmit, onExit, initialHistory = [] }, ref) => {
|
|
39
|
+
const config = readConfig();
|
|
40
|
+
const { exit } = useApp();
|
|
41
|
+
const [input, setInput] = useState('');
|
|
42
|
+
const [history, setHistory] = useState(initialHistory);
|
|
43
|
+
const [thinking, setThinking] = useState(false);
|
|
44
|
+
const [mode, setMode] = useState('Chat');
|
|
45
|
+
const [model, setModel] = useState('');
|
|
46
|
+
const [workspace, setWorkspace] = useState(process.cwd());
|
|
47
|
+
|
|
48
|
+
// Suggestions State
|
|
49
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
50
|
+
const inputRef = React.useRef(input);
|
|
51
|
+
const selectedIndexRef = React.useRef(selectedIndex);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
inputRef.current = input;
|
|
55
|
+
}, [input]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
selectedIndexRef.current = selectedIndex;
|
|
59
|
+
}, [selectedIndex]);
|
|
60
|
+
|
|
61
|
+
const showSuggestions = input.startsWith('/') && !input.includes(' ');
|
|
62
|
+
const suggestions = useMemo(() => {
|
|
63
|
+
if (!showSuggestions) return [];
|
|
64
|
+
const query = input.toLowerCase();
|
|
65
|
+
return SLASH_COMMANDS.filter(s => s.cmd.startsWith(query));
|
|
66
|
+
}, [input, showSuggestions]);
|
|
67
|
+
|
|
68
|
+
// Reset index when suggestions change
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
setSelectedIndex(0);
|
|
71
|
+
}, [suggestions.length]);
|
|
72
|
+
|
|
73
|
+
const lastSystemMessage = React.useRef('');
|
|
74
|
+
|
|
75
|
+
// Export methods to the outside world via ref
|
|
76
|
+
useImperativeHandle(ref, () => ({
|
|
77
|
+
appendMessage: (role, text, metadata = {}) => {
|
|
78
|
+
setHistory(prev => [...prev, { role, text, time: new Date(), ...metadata }]);
|
|
79
|
+
if (metadata.providerInfo) {
|
|
80
|
+
const { provider, model } = metadata.providerInfo;
|
|
81
|
+
setModel(model ? `${provider} • ${model}` : provider);
|
|
165
82
|
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// Line 1: Thinking / Status (Left) and Shortcut (Right)
|
|
190
|
-
const statusLine1 = blessed.text({
|
|
191
|
-
parent: statusBar,
|
|
192
|
-
top: 0, left: 1, right: 1,
|
|
193
|
-
height: 1,
|
|
194
|
-
tags: true,
|
|
195
|
-
content: `{#88aaff-fg}[Chat]{/} {#cc4444-fg}no sandbox{/}`,
|
|
196
|
-
style: { bg: '#10141c' }
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const shortcutHint = blessed.text({
|
|
200
|
-
parent: statusBar,
|
|
201
|
-
top: 0, right: 1,
|
|
202
|
-
height: 1,
|
|
203
|
-
tags: true,
|
|
204
|
-
content: `{gray-fg}? for shortcuts{/}`,
|
|
205
|
-
style: { bg: '#10141c' }
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Line 2: Action Hint (Left) and File Info (Right)
|
|
209
|
-
const statusLine2 = blessed.text({
|
|
210
|
-
parent: statusBar,
|
|
211
|
-
top: 1, left: 1, right: 1,
|
|
212
|
-
height: 1,
|
|
213
|
-
tags: true,
|
|
214
|
-
content: `{gray-fg}Shift+Tab to accept edits{/}`,
|
|
215
|
-
style: { bg: '#10141c' }
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const fileInfo = blessed.text({
|
|
219
|
-
parent: statusBar,
|
|
220
|
-
top: 1, right: 1,
|
|
221
|
-
height: 1,
|
|
222
|
-
tags: true,
|
|
223
|
-
content: `{gray-fg}path: ${workspacePath}{/}`,
|
|
224
|
-
style: { bg: '#10141c' }
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
let activeMode = 'Chat';
|
|
228
|
-
let spinnerIdx = 0;
|
|
229
|
-
const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
230
|
-
|
|
231
|
-
function formatTime(seconds) {
|
|
232
|
-
if (seconds < 60) return `${seconds}s`;
|
|
233
|
-
const mins = Math.floor(seconds / 60);
|
|
234
|
-
const secs = seconds % 60;
|
|
235
|
-
return `${mins}m ${secs}s`;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function updateStatusBar(thinkingText = null, secondsElapsed = 0) {
|
|
239
|
-
if (thinkingText) {
|
|
240
|
-
const char = spinnerChars[spinnerIdx % spinnerChars.length];
|
|
241
|
-
spinnerIdx++;
|
|
242
|
-
statusLine1.setContent(`{#88e0b0-fg}${char}{/} Thinking... {gray-fg}(esc to cancel, ${formatTime(secondsElapsed)}){/}`);
|
|
243
|
-
} else {
|
|
244
|
-
statusLine1.setContent(`${activeMode === 'Code' ? '{#ffd166-fg}[Code]{/}' : '{#88aaff-fg}[Chat]{/}'} {#cc4444-fg}no sandbox{/}`);
|
|
245
|
-
}
|
|
246
|
-
screen.render();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function setMode(mode) {
|
|
250
|
-
activeMode = mode === 'Code' ? 'Code' : 'Chat';
|
|
251
|
-
updateStatusBar(null);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function updateStatusModel(newModel) {
|
|
255
|
-
if (!newModel) return;
|
|
256
|
-
shortcutHint.setContent(`{gray-fg}${newModel} · ? for shortcuts{/}`);
|
|
257
|
-
screen.render();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** Update workspace name in status bar */
|
|
261
|
-
function updateWorkspace(newPath) {
|
|
262
|
-
if (!newPath) return;
|
|
263
|
-
fileInfo.setContent(`{gray-fg}path: ${newPath}{/}`);
|
|
264
|
-
screen.render();
|
|
265
|
-
}
|
|
266
|
-
updateStatusBar();
|
|
267
|
-
|
|
268
|
-
// ─── Append widgets to screen ─────────────────────────────────────────────
|
|
269
|
-
screen.append(banner);
|
|
270
|
-
screen.append(subBanner);
|
|
271
|
-
screen.append(chatBox);
|
|
272
|
-
screen.append(hintBar);
|
|
273
|
-
screen.append(inputBox);
|
|
274
|
-
screen.append(statusBar);
|
|
275
|
-
screen.append(placeholderWidget); // sibling on top of inputBox
|
|
276
|
-
|
|
277
|
-
// Suggestion List and Approval Dialog remain same ...
|
|
278
|
-
|
|
279
|
-
// ─── Suggestion List ──────────────────────────────────────────────────────
|
|
280
|
-
const commandList = blessed.list({
|
|
281
|
-
parent: screen,
|
|
282
|
-
bottom: 6,
|
|
283
|
-
left: 3,
|
|
284
|
-
width: '64%',
|
|
285
|
-
height: 8,
|
|
286
|
-
tags: true,
|
|
287
|
-
keys: false, // We will handle keys manually to keep focus on input
|
|
288
|
-
vi: false,
|
|
289
|
-
hidden: true,
|
|
290
|
-
border: { type: 'line', fg: '#88e0b0' },
|
|
291
|
-
style: {
|
|
292
|
-
bg: '#10141c',
|
|
293
|
-
fg: '#ffffff',
|
|
294
|
-
selected: {
|
|
295
|
-
bg: '#22352f',
|
|
296
|
-
fg: '#88e0b0',
|
|
297
|
-
bold: true
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
let activeSuggestions = [];
|
|
303
|
-
const approvalDialog = blessed.question({
|
|
304
|
-
parent: screen,
|
|
305
|
-
tags: true,
|
|
306
|
-
border: { type: 'line', fg: '#88e0b0' },
|
|
307
|
-
style: {
|
|
308
|
-
bg: '#10141c',
|
|
309
|
-
fg: '#ffffff',
|
|
310
|
-
border: { fg: '#88e0b0' }
|
|
311
|
-
},
|
|
312
|
-
width: '80%',
|
|
313
|
-
height: 12, // Fixed height to avoid 'shrink' miscalculation with buttons
|
|
314
|
-
top: 'center',
|
|
315
|
-
left: 'center',
|
|
316
|
-
label: ' Approval ',
|
|
317
|
-
hidden: true
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
function updateSuggestions(filter = '') {
|
|
321
|
-
activeSuggestions = SLASH_COMMANDS.filter(cmd =>
|
|
322
|
-
cmd.name.toLowerCase().startsWith(filter.toLowerCase())
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
if (activeSuggestions.length === 0) {
|
|
326
|
-
commandList.hide();
|
|
327
|
-
screen.render();
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const items = activeSuggestions.map(cmd =>
|
|
332
|
-
` {bold}${cmd.name}{/} {gray-fg}${cmd.desc}{/}`
|
|
333
|
-
);
|
|
334
|
-
commandList.setItems(items);
|
|
335
|
-
commandList.select(0);
|
|
336
|
-
commandList.show();
|
|
337
|
-
commandList.setFront();
|
|
338
|
-
screen.render();
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// ─── Input events ─────────────────────────────────────────────────────────
|
|
343
|
-
|
|
344
|
-
// ─── Input events ─────────────────────────────────────────────────────────
|
|
345
|
-
let lastListVisible = false;
|
|
346
|
-
|
|
347
|
-
// Consolidated key handling
|
|
348
|
-
inputBox.on('element keypress', (el, ch, key) => {
|
|
349
|
-
refreshInputStyles();
|
|
350
|
-
// 1. Handle placeholder visibility
|
|
351
|
-
if (!key.ctrl && !key.meta && key.name !== 'enter' && key.name !== 'tab') {
|
|
352
|
-
if (ch) hidePlaceholder();
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// 2. Handle suggestion list navigation
|
|
356
|
-
if (!commandList.hidden) {
|
|
357
|
-
if (key.name === 'up') {
|
|
358
|
-
commandList.up();
|
|
359
|
-
screen.render();
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
|
-
if (key.name === 'down') {
|
|
363
|
-
commandList.down();
|
|
364
|
-
screen.render();
|
|
365
|
-
return false;
|
|
366
|
-
}
|
|
367
|
-
if (key.name === 'escape') {
|
|
368
|
-
commandList.hide();
|
|
369
|
-
lastListVisible = false;
|
|
370
|
-
screen.render();
|
|
371
|
-
return false;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// 3. Logic for suggestions and placeholder after key is processed
|
|
376
|
-
setImmediate(() => {
|
|
377
|
-
refreshInputStyles();
|
|
378
|
-
const val = (inputBox.getValue ? inputBox.getValue() : inputBox.value) || '';
|
|
379
|
-
const isCommand = val.startsWith('/') && !val.includes(' ');
|
|
380
|
-
|
|
381
|
-
// Only render if visibility changed or list is updated
|
|
382
|
-
if (isCommand) {
|
|
383
|
-
updateSuggestions(val);
|
|
384
|
-
lastListVisible = true;
|
|
385
|
-
} else if (lastListVisible) {
|
|
386
|
-
commandList.hide();
|
|
387
|
-
lastListVisible = false;
|
|
388
|
-
screen.render();
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (!val.trim()) {
|
|
392
|
-
showPlaceholder();
|
|
393
|
-
} else {
|
|
394
|
-
hidePlaceholder();
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
inputBox.on('focus', () => {
|
|
400
|
-
refreshInputStyles();
|
|
401
|
-
screen.render();
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
inputBox.on('keypress', () => {
|
|
405
|
-
applyTerminalInputAttrs();
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
// Restore focus to inputBox when clicked or when screen is clicked
|
|
409
|
-
screen.on('click', () => {
|
|
410
|
-
if (!approvalDialog.visible) {
|
|
411
|
-
inputBox.focus();
|
|
412
|
-
screen.render();
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
inputBox.on('click', () => {
|
|
417
|
-
inputBox.focus();
|
|
418
|
-
screen.render();
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
// Submit or Select Suggestion on Enter
|
|
423
|
-
inputBox.on('submit', (value) => {
|
|
424
|
-
if (!commandList.hidden) {
|
|
425
|
-
const selected = activeSuggestions[commandList.selected];
|
|
426
|
-
if (selected) {
|
|
427
|
-
inputBox.setValue(selected.name + ' ');
|
|
428
|
-
commandList.hide();
|
|
429
|
-
hidePlaceholder();
|
|
430
|
-
inputBox.focus();
|
|
431
|
-
inputBox.readInput(); // Re-focus to continue typing
|
|
432
|
-
refreshInputStyles();
|
|
433
|
-
screen.render();
|
|
434
|
-
return; // Don't submit yet, let user add args or press enter again
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const raw = value || '';
|
|
439
|
-
const text = raw.trim();
|
|
440
|
-
if (!text) {
|
|
441
|
-
inputBox.clearValue();
|
|
442
|
-
showPlaceholder();
|
|
443
|
-
inputBox.focus();
|
|
444
|
-
inputBox.readInput(); // Re-focus to continue typing
|
|
445
|
-
refreshInputStyles();
|
|
446
|
-
screen.render();
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Clear input and restore placeholder
|
|
451
|
-
inputBox.clearValue();
|
|
452
|
-
showPlaceholder();
|
|
453
|
-
inputBox.focus();
|
|
454
|
-
inputBox.readInput(); // Explicitly restart reading
|
|
455
|
-
refreshInputStyles();
|
|
456
|
-
screen.render();
|
|
457
|
-
|
|
458
|
-
if (text.toLowerCase() === 'exit' || text.toLowerCase() === 'quit') {
|
|
459
|
-
onExit();
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
onSubmit(text);
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
// Shift+Enter = newline in input
|
|
467
|
-
// Ctrl+C — double-press to exit
|
|
468
|
-
let ctrlCPressed = false;
|
|
469
|
-
let ctrlCTimer = null;
|
|
470
|
-
screen.key(['C-c'], () => {
|
|
471
|
-
if (ctrlCPressed) {
|
|
472
|
-
clearTimeout(ctrlCTimer);
|
|
473
|
-
onExit();
|
|
474
|
-
} else {
|
|
475
|
-
ctrlCPressed = true;
|
|
476
|
-
hintBar.setContent(`{bold}{yellow-fg} Press Ctrl+C again to exit.{/} {gray-fg}(or type 'exit'){/}`);
|
|
477
|
-
screen.render();
|
|
478
|
-
ctrlCTimer = setTimeout(() => {
|
|
479
|
-
ctrlCPressed = false;
|
|
480
|
-
hintBar.setContent(HINT_DEFAULT);
|
|
481
|
-
screen.render();
|
|
482
|
-
}, 2000);
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
// ESC — exit immediately
|
|
487
|
-
screen.key(['escape'], () => {
|
|
488
|
-
onExit();
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
// ─── Clipboard copy (Ctrl+Y) ──────────────────────────────────────────────
|
|
492
|
-
function copyToClipboard(text) {
|
|
493
|
-
// Try xclip first, then xsel as fallback
|
|
494
|
-
const tools = [
|
|
495
|
-
`echo ${JSON.stringify(text)} | xclip -selection clipboard`,
|
|
496
|
-
`echo ${JSON.stringify(text)} | xsel --clipboard --input`
|
|
497
|
-
];
|
|
498
|
-
for (const cmd of tools) {
|
|
499
|
-
try {
|
|
500
|
-
execSync(cmd, { stdio: 'pipe' });
|
|
501
|
-
return true;
|
|
502
|
-
} catch (_) {}
|
|
503
|
-
}
|
|
504
|
-
return false;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function flashHint(msg, durationMs = 2000) {
|
|
508
|
-
hintBar.setContent(msg);
|
|
509
|
-
screen.render();
|
|
510
|
-
setTimeout(() => {
|
|
511
|
-
hintBar.setContent(HINT_DEFAULT);
|
|
512
|
-
screen.render();
|
|
513
|
-
}, durationMs);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
screen.key(['C-y'], () => {
|
|
517
|
-
if (!lastAssistantResponse) {
|
|
518
|
-
flashHint(`{yellow-fg} No response to copy yet.{/}`);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
const ok = copyToClipboard(lastAssistantResponse);
|
|
522
|
-
if (ok) {
|
|
523
|
-
flashHint(`{#88e0b0-fg} ✓ Copied to clipboard!{/}`);
|
|
524
|
-
} else {
|
|
525
|
-
flashHint(`{red-fg} ✖ Copy failed. Install xclip: sudo apt install xclip{/}`, 3000);
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
// ─── Initial render ───────────────────────────────────────────────────────
|
|
530
|
-
inputBox.focus();
|
|
531
|
-
inputBox.readInput(); // Initial start
|
|
532
|
-
refreshInputStyles();
|
|
533
|
-
screen.render();
|
|
534
|
-
|
|
535
|
-
// ─── Public API ───────────────────────────────────────────────────────────
|
|
536
|
-
|
|
537
|
-
// Track last assistant response for clipboard copy
|
|
538
|
-
let lastAssistantResponse = '';
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* @param {'user'|'assistant'|'system'|'error'} role
|
|
542
|
-
* @param {string} text
|
|
543
|
-
* @param {string} timestamp - ISO string or Date object
|
|
544
|
-
*/
|
|
545
|
-
function wrapLineSmart(line, width) {
|
|
546
|
-
if (line.length <= width) return [line];
|
|
547
|
-
if (!line.includes(' ')) {
|
|
548
|
-
const pieces = [];
|
|
549
|
-
for (let index = 0; index < line.length; index += width) {
|
|
550
|
-
pieces.push(line.slice(index, index + width));
|
|
551
|
-
}
|
|
552
|
-
return pieces;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
const words = line.split(/\s+/);
|
|
556
|
-
const lines = [];
|
|
557
|
-
let current = '';
|
|
558
|
-
for (const word of words) {
|
|
559
|
-
if (word.length > width) {
|
|
560
|
-
if (current) {
|
|
561
|
-
lines.push(current);
|
|
562
|
-
current = '';
|
|
563
|
-
}
|
|
564
|
-
for (let index = 0; index < word.length; index += width) {
|
|
565
|
-
const slice = word.slice(index, index + width);
|
|
566
|
-
if (slice.length === width) {
|
|
567
|
-
lines.push(slice);
|
|
83
|
+
},
|
|
84
|
+
setThinking: (val) => setThinking(val),
|
|
85
|
+
setMode: (val) => setMode(val),
|
|
86
|
+
updateStatusModel: (val) => setModel(val),
|
|
87
|
+
updateWorkspace: (val) => setWorkspace(val),
|
|
88
|
+
appendCodeStep: (info) => {
|
|
89
|
+
let text = '';
|
|
90
|
+
let label = 'System';
|
|
91
|
+
let labelColor = 'blueBright';
|
|
92
|
+
let isThought = false;
|
|
93
|
+
|
|
94
|
+
if (typeof info === 'string') {
|
|
95
|
+
text = info;
|
|
96
|
+
} else {
|
|
97
|
+
const { action, phase, target, message, thought } = info;
|
|
98
|
+
if (thought) {
|
|
99
|
+
text = thought;
|
|
100
|
+
label = 'Thinking';
|
|
101
|
+
labelColor = 'gray';
|
|
102
|
+
isThought = true;
|
|
103
|
+
} else if (action === 'thinking' || phase === 'thinking') {
|
|
104
|
+
return;
|
|
568
105
|
} else {
|
|
569
|
-
|
|
106
|
+
label = action || phase || 'Action';
|
|
107
|
+
text = target || message || '';
|
|
108
|
+
if (!text) return;
|
|
109
|
+
|
|
110
|
+
// Color coding for specific actions
|
|
111
|
+
if (label.includes('search')) labelColor = 'yellowBright';
|
|
112
|
+
else if (label.includes('file') || label.includes('path')) labelColor = 'cyanBright';
|
|
113
|
+
else if (label.includes('write') || label.includes('edit') || label.includes('patch')) labelColor = 'greenBright';
|
|
114
|
+
else if (label.includes('shell') || label.includes('run')) labelColor = 'magentaBright';
|
|
570
115
|
}
|
|
571
116
|
}
|
|
572
|
-
continue;
|
|
573
|
-
}
|
|
574
117
|
|
|
575
|
-
|
|
576
|
-
current
|
|
577
|
-
|
|
118
|
+
const fullText = `[${label}] ${text}`;
|
|
119
|
+
if (fullText === lastSystemMessage.current) return;
|
|
120
|
+
lastSystemMessage.current = fullText;
|
|
121
|
+
|
|
122
|
+
setHistory(prev => [...prev, {
|
|
123
|
+
role: 'system',
|
|
124
|
+
label,
|
|
125
|
+
labelColor,
|
|
126
|
+
text,
|
|
127
|
+
isThought,
|
|
128
|
+
time: new Date()
|
|
129
|
+
}]);
|
|
578
130
|
}
|
|
131
|
+
}));
|
|
579
132
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
if (current) lines.push(current);
|
|
588
|
-
return lines;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function wrapText(str, width) {
|
|
592
|
-
const lines = [];
|
|
593
|
-
const originalLines = String(str).split('\n');
|
|
594
|
-
for (const line of originalLines) {
|
|
595
|
-
if (line.length === 0) {
|
|
596
|
-
lines.push('');
|
|
597
|
-
continue;
|
|
598
|
-
}
|
|
599
|
-
lines.push(...wrapLineSmart(line, width));
|
|
600
|
-
}
|
|
601
|
-
return lines;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function appendMessage(role, text, timestamp = null) {
|
|
605
|
-
const now = timestamp ? new Date(timestamp) : new Date();
|
|
606
|
-
const timeStr = now.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
607
|
-
const maxLineWidth = Math.max(screen.width - 20, 36);
|
|
608
|
-
const lines = wrapText(text, maxLineWidth);
|
|
609
|
-
|
|
610
|
-
if (role === 'user') {
|
|
611
|
-
chatBox.log(``);
|
|
612
|
-
chatBox.log(` {bold}{#88e0b0-fg}You{/} {gray-fg}${timeStr}{/}`);
|
|
613
|
-
lines.forEach(l => chatBox.log(` {#88e0b0-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
614
|
-
} else if (role === 'assistant') {
|
|
615
|
-
lastAssistantResponse = text;
|
|
616
|
-
chatBox.log(``);
|
|
617
|
-
chatBox.log(` {bold}{#d4a8ff-fg}Mint{/} {gray-fg}${timeStr}{/}`);
|
|
618
|
-
lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
619
|
-
} else if (role === 'system') {
|
|
620
|
-
const displayTag = text.startsWith('Action:')
|
|
621
|
-
? '{#88e0b0-fg}Action{/}'
|
|
622
|
-
: text.startsWith('[Code]')
|
|
623
|
-
? '{#ffd166-fg}Code{/}'
|
|
624
|
-
: '{#8ba0ff-fg}System{/}';
|
|
625
|
-
const cleanText = text.replace(/^(Action:|System:)\s*/, '');
|
|
626
|
-
const systemLines = wrapText(cleanText, maxLineWidth - 4);
|
|
627
|
-
chatBox.log(``);
|
|
628
|
-
chatBox.log(` {bold}${displayTag}{/}`);
|
|
629
|
-
systemLines.forEach(l => chatBox.log(` {#95a2b8-fg}${l}{/}`));
|
|
630
|
-
} else if (role === 'error') {
|
|
631
|
-
chatBox.log(``);
|
|
632
|
-
chatBox.log(` {bold}{#ff6b6b-fg}Error{/} {gray-fg}${timeStr}{/}`);
|
|
633
|
-
lines.forEach(l => chatBox.log(` {#7a2e2e-fg}▏{/} {#ff7d7d-fg}${l}{/}`));
|
|
634
|
-
}
|
|
635
|
-
screen.render();
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Opens a streaming message bubble for the assistant.
|
|
640
|
-
* Returns { appendChunk(text), finalize(timestamp) } for typewriter rendering.
|
|
641
|
-
* Usage:
|
|
642
|
-
* const stream = streamMessage('assistant');
|
|
643
|
-
* stream.appendChunk('Hello'); stream.appendChunk(' World');
|
|
644
|
-
* stream.finalize(timestamp);
|
|
645
|
-
*/
|
|
646
|
-
function streamMessage(role = 'assistant') {
|
|
647
|
-
const now = new Date();
|
|
648
|
-
const timeStr = now.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
649
|
-
const maxLineWidth = Math.max(screen.width - 20, 36);
|
|
650
|
-
|
|
651
|
-
// Print the header bubble once
|
|
652
|
-
chatBox.log('');
|
|
653
|
-
if (role === 'assistant') {
|
|
654
|
-
chatBox.log(` {bold}{#d4a8ff-fg}Mint{/} {gray-fg}${timeStr}{/}`);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
let buffer = ''; // accumulates the full response text
|
|
658
|
-
let lineBuffer = ''; // current partial line being built
|
|
659
|
-
let lineRendered = false; // whether we already pushed the first line prefix
|
|
660
|
-
|
|
661
|
-
function flushLine(force = false) {
|
|
662
|
-
// Flush content that fits on one line-width or when forced
|
|
663
|
-
if (!lineBuffer && !force) return;
|
|
664
|
-
if (!lineRendered) {
|
|
665
|
-
chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
|
|
666
|
-
lineRendered = true;
|
|
667
|
-
} else {
|
|
668
|
-
// Overwrite the last line by popping + re-pushing (blessed.log limitation)
|
|
669
|
-
// We can't truly overwrite, so we just keep appending new lines for each chunk.
|
|
670
|
-
// For large chunks, split on newline and emit per-line.
|
|
671
|
-
chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
|
|
133
|
+
// Handle exiting and keyboard navigation
|
|
134
|
+
useInput((inputStr, key) => {
|
|
135
|
+
if (key.escape || (key.ctrl && inputStr === 'c')) {
|
|
136
|
+
onExit();
|
|
137
|
+
exit();
|
|
672
138
|
}
|
|
673
|
-
screen.render();
|
|
674
|
-
}
|
|
675
139
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
lineRendered = true;
|
|
695
|
-
screen.render();
|
|
696
|
-
}
|
|
697
|
-
// Otherwise keep buffering the partial line
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function finalize(timestamp = null) {
|
|
702
|
-
// Flush remaining buffer
|
|
703
|
-
if (lineBuffer) {
|
|
704
|
-
const lines = wrapLineSmart(lineBuffer, maxLineWidth);
|
|
705
|
-
lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
706
|
-
lineBuffer = '';
|
|
707
|
-
}
|
|
708
|
-
// Track last response for clipboard
|
|
709
|
-
lastAssistantResponse = buffer;
|
|
710
|
-
screen.render();
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
return { appendChunk, finalize };
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function appendCodeStep(info) {
|
|
717
|
-
if (typeof info === 'string') {
|
|
718
|
-
appendMessage('system', `[Code] ${info}`);
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const { step, phase, action, target, message, thought } = info;
|
|
723
|
-
const maxLineWidth = Math.max(screen.width - 20, 36);
|
|
724
|
-
|
|
725
|
-
// Special handling for ask_user which needs a box style
|
|
726
|
-
if (action === 'ask_user') {
|
|
727
|
-
chatBox.log('');
|
|
728
|
-
chatBox.log(` {#88e0b0-fg}✓{/} {bold}Ask User{/}`);
|
|
729
|
-
const questionLines = wrapText(target || message || '', maxLineWidth - 6);
|
|
730
|
-
questionLines.forEach(l => chatBox.log(` {#95a2b8-fg}${l}{/}`));
|
|
731
|
-
screen.render();
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
let icon = '{#88e0b0-fg}✓{/}';
|
|
736
|
-
let label = action || phase;
|
|
737
|
-
let color = '{#ffffff-fg}';
|
|
738
|
-
|
|
739
|
-
// Map internal action names to display names seen in the screenshot
|
|
740
|
-
switch (action) {
|
|
741
|
-
case 'thinking':
|
|
742
|
-
if (phase === 'thinking' && !thought) {
|
|
743
|
-
// Initial "Thinking..." without a bubble
|
|
744
|
-
chatBox.log('');
|
|
745
|
-
chatBox.log(` {#ffd166-fg}* {bold}Thinking{/}`);
|
|
746
|
-
} else if (thought) {
|
|
747
|
-
// Show reasoning bubble
|
|
748
|
-
const thoughtLines = wrapText(thought, maxLineWidth - 6);
|
|
749
|
-
thoughtLines.forEach(l => chatBox.log(` {gray-fg}> ${l}{/}`));
|
|
140
|
+
const currentInput = inputRef.current;
|
|
141
|
+
const currentShowSuggestions = currentInput.startsWith('/') && !currentInput.includes(' ');
|
|
142
|
+
|
|
143
|
+
if (currentShowSuggestions) {
|
|
144
|
+
const query = currentInput.toLowerCase();
|
|
145
|
+
const currentSuggestions = SLASH_COMMANDS.filter(s => s.cmd.startsWith(query));
|
|
146
|
+
|
|
147
|
+
if (currentSuggestions.length > 0) {
|
|
148
|
+
if (key.upArrow) {
|
|
149
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : currentSuggestions.length - 1));
|
|
150
|
+
} else if (key.downArrow) {
|
|
151
|
+
setSelectedIndex(prev => (prev < currentSuggestions.length - 1 ? prev + 1 : 0));
|
|
152
|
+
} else if (key.tab || (key.return && currentInput.startsWith('/'))) {
|
|
153
|
+
const picked = currentSuggestions[selectedIndexRef.current];
|
|
154
|
+
if (picked) {
|
|
155
|
+
setInput(picked.cmd + ' ');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
750
158
|
}
|
|
751
|
-
screen.render();
|
|
752
|
-
return;
|
|
753
|
-
case 'ask_user': label = 'AskUser'; break;
|
|
754
|
-
case 'open_url': label = 'OpenURL'; break;
|
|
755
|
-
case 'open_app': label = 'OpenApp'; break;
|
|
756
|
-
case 'open_file': label = 'OpenFile'; break;
|
|
757
|
-
case 'open_folder': label = 'OpenFolder'; break;
|
|
758
|
-
case 'create_folder': label = 'CreateFolder'; break;
|
|
759
|
-
case 'system_info': label = 'SystemInfo'; break;
|
|
760
|
-
case 'system_automation': label = 'SystemAction'; break;
|
|
761
|
-
case 'web_search': label = 'WebSearch'; break;
|
|
762
|
-
case 'list_files':
|
|
763
|
-
case 'find_path': label = 'Explored'; break;
|
|
764
|
-
case 'read_file': label = 'ReadFile'; break;
|
|
765
|
-
case 'search_code': label = 'SearchText'; break;
|
|
766
|
-
case 'apply_patch':
|
|
767
|
-
case 'write_file': label = 'Edited'; break;
|
|
768
|
-
case 'run_shell': label = 'Ran command'; break;
|
|
769
|
-
case 'json_repair': icon = '*'; label = 'Repairing JSON'; color = '{#ffd166-fg}'; break;
|
|
770
|
-
case 'reviewer_start': label = 'Reviewing'; break;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const content = target || message || '';
|
|
774
|
-
chatBox.log(` ${icon} {bold}${label}{/} ${color}${content}{/}`);
|
|
775
|
-
screen.render();
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/** Show/hide thinking indicator in status bar */
|
|
779
|
-
function setThinking(active, secondsElapsed = 0) {
|
|
780
|
-
if (active) {
|
|
781
|
-
updateStatusBar('Thinking...', secondsElapsed);
|
|
782
|
-
} else {
|
|
783
|
-
updateStatusBar(null);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
/** Copy last assistant response to clipboard */
|
|
788
|
-
function copyLastResponse() {
|
|
789
|
-
if (!lastAssistantResponse) return false;
|
|
790
|
-
return copyToClipboard(lastAssistantResponse);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function requestApproval(request) {
|
|
794
|
-
return new Promise((resolve) => {
|
|
795
|
-
const typeLabel = request.type === 'shell'
|
|
796
|
-
? 'Shell Command'
|
|
797
|
-
: request.type === 'patch'
|
|
798
|
-
? 'Patch Edit'
|
|
799
|
-
: request.type === 'code_mode'
|
|
800
|
-
? 'Enter Code Mode'
|
|
801
|
-
: 'File Write';
|
|
802
|
-
const preview = request.preview || request.label || '';
|
|
803
|
-
const message = [
|
|
804
|
-
`{bold}${typeLabel}{/bold}`,
|
|
805
|
-
'',
|
|
806
|
-
preview,
|
|
807
|
-
'',
|
|
808
|
-
'Approve this action?',
|
|
809
|
-
'', // Extra lines to push buttons down and avoid overlapping
|
|
810
|
-
''
|
|
811
|
-
].join('\n');
|
|
812
|
-
|
|
813
|
-
// Temporarily stop reading input so the dialog can receive keys
|
|
814
|
-
if (inputBox._reading) {
|
|
815
|
-
inputBox.cancel();
|
|
816
159
|
}
|
|
817
|
-
|
|
818
|
-
approvalDialog.ask(message, (approved) => {
|
|
819
|
-
inputBox.focus();
|
|
820
|
-
inputBox.readInput(); // Ensure we resume reading after dialog
|
|
821
|
-
refreshInputStyles();
|
|
822
|
-
screen.render();
|
|
823
|
-
resolve(Boolean(approved));
|
|
824
|
-
});
|
|
825
160
|
});
|
|
826
|
-
}
|
|
827
161
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
162
|
+
const handleSubmit = (value) => {
|
|
163
|
+
const text = value.trim();
|
|
164
|
+
if (!text) return;
|
|
165
|
+
|
|
166
|
+
if (showSuggestions && suggestions.length > 0) {
|
|
167
|
+
const picked = suggestions[selectedIndex];
|
|
168
|
+
if (picked && text !== picked.cmd) {
|
|
169
|
+
setInput(picked.cmd + ' ');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
833
172
|
}
|
|
834
173
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
inputBox._label.setContent(oldLabel);
|
|
174
|
+
setInput('');
|
|
175
|
+
onSubmit(text);
|
|
176
|
+
};
|
|
177
|
+
|
|
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
|
+
}
|
|
186
|
+
|
|
187
|
+
let name = 'Mint';
|
|
188
|
+
let nameColor = 'greenBright';
|
|
851
189
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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';
|
|
199
|
+
}
|
|
861
200
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
+
)
|
|
209
|
+
);
|
|
210
|
+
}),
|
|
211
|
+
|
|
212
|
+
// Floating (Persistent) UI part
|
|
213
|
+
h(Box, { flexDirection: 'column' },
|
|
214
|
+
thinking && h(Box, { marginBottom: 1 },
|
|
215
|
+
h(Text, { color: 'yellow' }, '● Mint is thinking...')
|
|
216
|
+
),
|
|
217
|
+
|
|
218
|
+
// Suggestions Menu
|
|
219
|
+
showSuggestions && suggestions.length > 0 && h(Box, {
|
|
220
|
+
flexDirection: 'column',
|
|
221
|
+
borderStyle: 'single',
|
|
222
|
+
borderColor: 'gray',
|
|
223
|
+
paddingX: 1,
|
|
224
|
+
marginBottom: 0
|
|
225
|
+
},
|
|
226
|
+
suggestions.map((s, i) => h(Box, { key: s.cmd, flexDirection: 'row' },
|
|
227
|
+
h(Text, {
|
|
228
|
+
backgroundColor: i === selectedIndex ? 'green' : undefined,
|
|
229
|
+
color: i === selectedIndex ? 'white' : 'greenBright'
|
|
230
|
+
}, s.cmd.padEnd(12)),
|
|
231
|
+
h(Text, { color: 'gray' }, ` ${s.desc}`)
|
|
232
|
+
))
|
|
233
|
+
),
|
|
234
|
+
|
|
235
|
+
// 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
|
+
})
|
|
244
|
+
),
|
|
245
|
+
|
|
246
|
+
// Status Bar
|
|
247
|
+
h(Box, { justifyContent: 'space-between' },
|
|
248
|
+
h(Box, null,
|
|
249
|
+
h(Text, { color: 'cyan' }, `[${mode}] `),
|
|
250
|
+
h(Text, { color: 'magentaBright' }, (model || config.geminiModel || 'gemini').slice(0, 46))
|
|
251
|
+
),
|
|
252
|
+
h(Box, null,
|
|
253
|
+
h(Text, { color: 'gray' }, `path: ...${workspace.slice(-20)}`)
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
});
|
|
865
259
|
|
|
866
|
-
|
|
260
|
+
// Print banner once before rendering the main app-
|
|
261
|
+
console.log(`\x1b[38;5;121m\x1b[1m __ __ _ _ ___ _ ___ \x1b[0m`);
|
|
262
|
+
console.log(`\x1b[38;5;121m\x1b[1m| \\/ (_)_ __ | |_ / __| | |_ _|\x1b[0m`);
|
|
263
|
+
console.log(`\x1b[38;5;121m\x1b[1m| |\\/| | | '_ \\| _| (__| |__ | | \x1b[0m`);
|
|
264
|
+
console.log(`\x1b[38;5;121m\x1b[1m|_| |_|_|_| |_|\\__|\\___|____|___|\x1b[0m`);
|
|
265
|
+
console.log(`\x1b[90mType naturally to chat. Esc to exit.\x1b[0m\n`);
|
|
266
|
+
|
|
267
|
+
const ref = createRef();
|
|
268
|
+
render(h(App, { ref, ...options }));
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
appendMessage: (role, text, metadata) => ref.current?.appendMessage(role, text, metadata),
|
|
272
|
+
setThinking: (val) => ref.current?.setThinking(val),
|
|
273
|
+
setMode: (val) => ref.current?.setMode(val),
|
|
274
|
+
updateStatusModel: (val) => ref.current?.updateStatusModel(val),
|
|
275
|
+
updateWorkspace: (val) => ref.current?.updateWorkspace(val),
|
|
276
|
+
appendCodeStep: (info) => ref.current?.appendCodeStep(info),
|
|
277
|
+
streamMessage: () => {
|
|
278
|
+
let fullText = '';
|
|
279
|
+
return {
|
|
280
|
+
appendChunk: (chunk) => {
|
|
281
|
+
fullText += chunk;
|
|
282
|
+
},
|
|
283
|
+
finalize: () => {
|
|
284
|
+
ref.current?.appendMessage('assistant', fullText);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
copyLastResponse: () => false,
|
|
289
|
+
requestApproval: () => Promise.resolve(true),
|
|
290
|
+
askUser: () => Promise.resolve('')
|
|
291
|
+
};
|
|
867
292
|
}
|
|
868
293
|
|
|
869
294
|
module.exports = { createChatUI };
|