@pheem49/mint 1.4.1 → 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 +214 -142
- 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 +15 -8
- package/mint-cli.js +305 -195
- package/package.json +12 -4
- package/src/AI_Brain/Gemini_API.js +77 -20
- package/src/AI_Brain/agent_orchestrator.js +6 -6
- 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 +14 -3
- package/src/CLI/chat_router.js +21 -7
- package/src/CLI/chat_ui.js +264 -710
- package/src/CLI/code_agent.js +370 -124
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/list_features.js +5 -1
- 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,740 +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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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);
|
|
82
|
+
}
|
|
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;
|
|
105
|
+
} else {
|
|
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';
|
|
115
|
+
}
|
|
165
116
|
}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
applyTerminalInputAttrs();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function applyTerminalInputAttrs() {
|
|
172
|
-
try {
|
|
173
|
-
if (!screen || !screen.program || typeof inputBox.sattr !== 'function' || typeof screen.codeAttr !== 'function') {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const attr = inputBox.sattr(inputBox.style);
|
|
177
|
-
screen.program.write(screen.codeAttr(attr));
|
|
178
|
-
} catch (_) {}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ─── Status bar (3 columns: left / center / right) ──────────────────────
|
|
182
|
-
const statusBar = blessed.box({
|
|
183
|
-
bottom: 0, left: 1, width: '100%-2', height: 3,
|
|
184
|
-
tags: true,
|
|
185
|
-
style: { bg: '#10141c', fg: '#888888' },
|
|
186
|
-
border: { type: 'line', fg: '#222c38' }
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// Left: workspace info
|
|
190
|
-
const statusLeft = blessed.text({
|
|
191
|
-
parent: statusBar,
|
|
192
|
-
top: 0, left: 1,
|
|
193
|
-
width: '33%',
|
|
194
|
-
height: 1,
|
|
195
|
-
tags: true,
|
|
196
|
-
content: ` workspace {bold}(${workspaceName}){/bold}`,
|
|
197
|
-
style: { bg: '#10141c', fg: '#93a0b7' }
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// Center: mode + status
|
|
201
|
-
const statusCenter = blessed.text({
|
|
202
|
-
parent: statusBar,
|
|
203
|
-
top: 0,
|
|
204
|
-
left: 'center',
|
|
205
|
-
width: '44%',
|
|
206
|
-
height: 1,
|
|
207
|
-
align: 'center',
|
|
208
|
-
tags: true,
|
|
209
|
-
content: `{#88aaff-fg}[Chat]{/} {#cc4444-fg}no sandbox{/}`,
|
|
210
|
-
style: { bg: '#10141c', fg: '#888888' }
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Right: current model
|
|
214
|
-
const statusRight = blessed.text({
|
|
215
|
-
parent: statusBar,
|
|
216
|
-
top: 0, right: 1,
|
|
217
|
-
width: '33%',
|
|
218
|
-
height: 1,
|
|
219
|
-
align: 'right',
|
|
220
|
-
tags: true,
|
|
221
|
-
content: `{#88e0b0-fg}${modelName}{/}`,
|
|
222
|
-
style: { bg: '#10141c', fg: '#88e0b0' }
|
|
223
|
-
});
|
|
224
117
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
screen.render();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function setMode(mode) {
|
|
242
|
-
activeMode = mode === 'Code' ? 'Code' : 'Chat';
|
|
243
|
-
updateStatusBar(null);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Update model name in status bar (called after /models switch) */
|
|
247
|
-
function updateStatusModel(newModel) {
|
|
248
|
-
if (!newModel) return;
|
|
249
|
-
statusRight.setContent(`{#88e0b0-fg}${newModel}{/}`);
|
|
250
|
-
screen.render();
|
|
251
|
-
}
|
|
252
|
-
updateStatusBar();
|
|
253
|
-
|
|
254
|
-
// ─── Append widgets to screen ─────────────────────────────────────────────
|
|
255
|
-
screen.append(banner);
|
|
256
|
-
screen.append(subBanner);
|
|
257
|
-
screen.append(chatBox);
|
|
258
|
-
screen.append(hintBar);
|
|
259
|
-
screen.append(inputBox);
|
|
260
|
-
screen.append(statusBar);
|
|
261
|
-
screen.append(placeholderWidget); // sibling on top of inputBox
|
|
262
|
-
|
|
263
|
-
// ─── Suggestion List ──────────────────────────────────────────────────────
|
|
264
|
-
const commandList = blessed.list({
|
|
265
|
-
parent: screen,
|
|
266
|
-
bottom: 6,
|
|
267
|
-
left: 3,
|
|
268
|
-
width: '64%',
|
|
269
|
-
height: 8,
|
|
270
|
-
tags: true,
|
|
271
|
-
keys: false, // We will handle keys manually to keep focus on input
|
|
272
|
-
vi: false,
|
|
273
|
-
hidden: true,
|
|
274
|
-
border: { type: 'line', fg: '#88e0b0' },
|
|
275
|
-
style: {
|
|
276
|
-
bg: '#10141c',
|
|
277
|
-
fg: '#ffffff',
|
|
278
|
-
selected: {
|
|
279
|
-
bg: '#22352f',
|
|
280
|
-
fg: '#88e0b0',
|
|
281
|
-
bold: true
|
|
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
|
+
}]);
|
|
282
130
|
}
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
let activeSuggestions = [];
|
|
287
|
-
const approvalDialog = blessed.question({
|
|
288
|
-
parent: screen,
|
|
289
|
-
tags: true,
|
|
290
|
-
border: { type: 'line', fg: '#88e0b0' },
|
|
291
|
-
style: {
|
|
292
|
-
bg: '#10141c',
|
|
293
|
-
fg: '#ffffff',
|
|
294
|
-
border: { fg: '#88e0b0' }
|
|
295
|
-
},
|
|
296
|
-
width: '80%',
|
|
297
|
-
height: 12, // Fixed height to avoid 'shrink' miscalculation with buttons
|
|
298
|
-
top: 'center',
|
|
299
|
-
left: 'center',
|
|
300
|
-
label: ' Approval ',
|
|
301
|
-
hidden: true
|
|
302
|
-
});
|
|
131
|
+
}));
|
|
303
132
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (activeSuggestions.length === 0) {
|
|
310
|
-
commandList.hide();
|
|
311
|
-
screen.render();
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const items = activeSuggestions.map(cmd =>
|
|
316
|
-
` {bold}${cmd.name}{/} {gray-fg}${cmd.desc}{/}`
|
|
317
|
-
);
|
|
318
|
-
commandList.setItems(items);
|
|
319
|
-
commandList.select(0);
|
|
320
|
-
commandList.show();
|
|
321
|
-
commandList.setFront();
|
|
322
|
-
screen.render();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// ─── Input events ─────────────────────────────────────────────────────────
|
|
327
|
-
|
|
328
|
-
// ─── Input events ─────────────────────────────────────────────────────────
|
|
329
|
-
let lastListVisible = false;
|
|
330
|
-
|
|
331
|
-
// Consolidated key handling
|
|
332
|
-
inputBox.on('element keypress', (el, ch, key) => {
|
|
333
|
-
refreshInputStyles();
|
|
334
|
-
// 1. Handle placeholder visibility
|
|
335
|
-
if (!key.ctrl && !key.meta && key.name !== 'enter' && key.name !== 'tab') {
|
|
336
|
-
if (ch) hidePlaceholder();
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// 2. Handle suggestion list navigation
|
|
340
|
-
if (!commandList.hidden) {
|
|
341
|
-
if (key.name === 'up') {
|
|
342
|
-
commandList.up();
|
|
343
|
-
screen.render();
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
if (key.name === 'down') {
|
|
347
|
-
commandList.down();
|
|
348
|
-
screen.render();
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
if (key.name === 'escape') {
|
|
352
|
-
commandList.hide();
|
|
353
|
-
lastListVisible = false;
|
|
354
|
-
screen.render();
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// 3. Logic for suggestions and placeholder after key is processed
|
|
360
|
-
setImmediate(() => {
|
|
361
|
-
refreshInputStyles();
|
|
362
|
-
const val = (inputBox.getValue ? inputBox.getValue() : inputBox.value) || '';
|
|
363
|
-
const isCommand = val.startsWith('/') && !val.includes(' ');
|
|
364
|
-
|
|
365
|
-
// Only render if visibility changed or list is updated
|
|
366
|
-
if (isCommand) {
|
|
367
|
-
updateSuggestions(val);
|
|
368
|
-
lastListVisible = true;
|
|
369
|
-
} else if (lastListVisible) {
|
|
370
|
-
commandList.hide();
|
|
371
|
-
lastListVisible = false;
|
|
372
|
-
screen.render();
|
|
133
|
+
// Handle exiting and keyboard navigation
|
|
134
|
+
useInput((inputStr, key) => {
|
|
135
|
+
if (key.escape || (key.ctrl && inputStr === 'c')) {
|
|
136
|
+
onExit();
|
|
137
|
+
exit();
|
|
373
138
|
}
|
|
374
139
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
}
|
|
158
|
+
}
|
|
379
159
|
}
|
|
380
160
|
});
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
inputBox.on('focus', () => {
|
|
384
|
-
refreshInputStyles();
|
|
385
|
-
screen.render();
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
inputBox.on('keypress', () => {
|
|
389
|
-
applyTerminalInputAttrs();
|
|
390
|
-
});
|
|
391
|
-
|
|
392
161
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const selected = activeSuggestions[commandList.selected];
|
|
397
|
-
if (selected) {
|
|
398
|
-
inputBox.setValue(selected.name + ' ');
|
|
399
|
-
commandList.hide();
|
|
400
|
-
hidePlaceholder();
|
|
401
|
-
inputBox.focus();
|
|
402
|
-
inputBox.readInput(); // Re-focus to continue typing
|
|
403
|
-
refreshInputStyles();
|
|
404
|
-
screen.render();
|
|
405
|
-
return; // Don't submit yet, let user add args or press enter again
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const raw = value || '';
|
|
410
|
-
const text = raw.trim();
|
|
411
|
-
if (!text) {
|
|
412
|
-
inputBox.clearValue();
|
|
413
|
-
showPlaceholder();
|
|
414
|
-
inputBox.focus();
|
|
415
|
-
inputBox.readInput(); // Re-focus to continue typing
|
|
416
|
-
refreshInputStyles();
|
|
417
|
-
screen.render();
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Clear input and restore placeholder
|
|
422
|
-
inputBox.clearValue();
|
|
423
|
-
showPlaceholder();
|
|
424
|
-
inputBox.focus();
|
|
425
|
-
inputBox.readInput(); // Explicitly restart reading
|
|
426
|
-
refreshInputStyles();
|
|
427
|
-
screen.render();
|
|
428
|
-
|
|
429
|
-
if (text.toLowerCase() === 'exit' || text.toLowerCase() === 'quit') {
|
|
430
|
-
onExit();
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
onSubmit(text);
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
// Shift+Enter = newline in input
|
|
438
|
-
// Ctrl+C — double-press to exit
|
|
439
|
-
let ctrlCPressed = false;
|
|
440
|
-
let ctrlCTimer = null;
|
|
441
|
-
screen.key(['C-c'], () => {
|
|
442
|
-
if (ctrlCPressed) {
|
|
443
|
-
clearTimeout(ctrlCTimer);
|
|
444
|
-
onExit();
|
|
445
|
-
} else {
|
|
446
|
-
ctrlCPressed = true;
|
|
447
|
-
hintBar.setContent(`{bold}{yellow-fg} Press Ctrl+C again to exit.{/} {gray-fg}(or type 'exit'){/}`);
|
|
448
|
-
screen.render();
|
|
449
|
-
ctrlCTimer = setTimeout(() => {
|
|
450
|
-
ctrlCPressed = false;
|
|
451
|
-
hintBar.setContent(HINT_DEFAULT);
|
|
452
|
-
screen.render();
|
|
453
|
-
}, 2000);
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
// ESC — exit immediately
|
|
458
|
-
screen.key(['escape'], () => {
|
|
459
|
-
onExit();
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
// ─── Clipboard copy (Ctrl+Y) ──────────────────────────────────────────────
|
|
463
|
-
function copyToClipboard(text) {
|
|
464
|
-
// Try xclip first, then xsel as fallback
|
|
465
|
-
const tools = [
|
|
466
|
-
`echo ${JSON.stringify(text)} | xclip -selection clipboard`,
|
|
467
|
-
`echo ${JSON.stringify(text)} | xsel --clipboard --input`
|
|
468
|
-
];
|
|
469
|
-
for (const cmd of tools) {
|
|
470
|
-
try {
|
|
471
|
-
execSync(cmd, { stdio: 'pipe' });
|
|
472
|
-
return true;
|
|
473
|
-
} catch (_) {}
|
|
474
|
-
}
|
|
475
|
-
return false;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function flashHint(msg, durationMs = 2000) {
|
|
479
|
-
hintBar.setContent(msg);
|
|
480
|
-
screen.render();
|
|
481
|
-
setTimeout(() => {
|
|
482
|
-
hintBar.setContent(HINT_DEFAULT);
|
|
483
|
-
screen.render();
|
|
484
|
-
}, durationMs);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
screen.key(['C-y'], () => {
|
|
488
|
-
if (!lastAssistantResponse) {
|
|
489
|
-
flashHint(`{yellow-fg} No response to copy yet.{/}`);
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
const ok = copyToClipboard(lastAssistantResponse);
|
|
493
|
-
if (ok) {
|
|
494
|
-
flashHint(`{#88e0b0-fg} ✓ Copied to clipboard!{/}`);
|
|
495
|
-
} else {
|
|
496
|
-
flashHint(`{red-fg} ✖ Copy failed. Install xclip: sudo apt install xclip{/}`, 3000);
|
|
497
|
-
}
|
|
498
|
-
});
|
|
162
|
+
const handleSubmit = (value) => {
|
|
163
|
+
const text = value.trim();
|
|
164
|
+
if (!text) return;
|
|
499
165
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
// ─── Public API ───────────────────────────────────────────────────────────
|
|
507
|
-
|
|
508
|
-
// Track last assistant response for clipboard copy
|
|
509
|
-
let lastAssistantResponse = '';
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* @param {'user'|'assistant'|'system'|'error'} role
|
|
513
|
-
* @param {string} text
|
|
514
|
-
* @param {string} timestamp - ISO string or Date object
|
|
515
|
-
*/
|
|
516
|
-
function wrapLineSmart(line, width) {
|
|
517
|
-
if (line.length <= width) return [line];
|
|
518
|
-
if (!line.includes(' ')) {
|
|
519
|
-
const pieces = [];
|
|
520
|
-
for (let index = 0; index < line.length; index += width) {
|
|
521
|
-
pieces.push(line.slice(index, index + width));
|
|
522
|
-
}
|
|
523
|
-
return pieces;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const words = line.split(/\s+/);
|
|
527
|
-
const lines = [];
|
|
528
|
-
let current = '';
|
|
529
|
-
for (const word of words) {
|
|
530
|
-
if (word.length > width) {
|
|
531
|
-
if (current) {
|
|
532
|
-
lines.push(current);
|
|
533
|
-
current = '';
|
|
166
|
+
if (showSuggestions && suggestions.length > 0) {
|
|
167
|
+
const picked = suggestions[selectedIndex];
|
|
168
|
+
if (picked && text !== picked.cmd) {
|
|
169
|
+
setInput(picked.cmd + ' ');
|
|
170
|
+
return;
|
|
534
171
|
}
|
|
535
|
-
for (let index = 0; index < word.length; index += width) {
|
|
536
|
-
const slice = word.slice(index, index + width);
|
|
537
|
-
if (slice.length === width) {
|
|
538
|
-
lines.push(slice);
|
|
539
|
-
} else {
|
|
540
|
-
current = slice;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
continue;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (!current) {
|
|
547
|
-
current = word;
|
|
548
|
-
continue;
|
|
549
172
|
}
|
|
550
173
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const lines = [];
|
|
564
|
-
const originalLines = String(str).split('\n');
|
|
565
|
-
for (const line of originalLines) {
|
|
566
|
-
if (line.length === 0) {
|
|
567
|
-
lines.push('');
|
|
568
|
-
continue;
|
|
569
|
-
}
|
|
570
|
-
lines.push(...wrapLineSmart(line, width));
|
|
571
|
-
}
|
|
572
|
-
return lines;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function appendMessage(role, text, timestamp = null) {
|
|
576
|
-
const now = timestamp ? new Date(timestamp) : new Date();
|
|
577
|
-
const timeStr = now.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
578
|
-
const maxLineWidth = Math.max(screen.width - 20, 36);
|
|
579
|
-
const lines = wrapText(text, maxLineWidth);
|
|
580
|
-
|
|
581
|
-
if (role === 'user') {
|
|
582
|
-
chatBox.log(``);
|
|
583
|
-
chatBox.log(` {bold}{#88e0b0-fg}You{/} {gray-fg}${timeStr}{/}`);
|
|
584
|
-
lines.forEach(l => chatBox.log(` {#88e0b0-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
585
|
-
} else if (role === 'assistant') {
|
|
586
|
-
lastAssistantResponse = text;
|
|
587
|
-
chatBox.log(``);
|
|
588
|
-
chatBox.log(` {bold}{#d4a8ff-fg}Mint{/} {gray-fg}${timeStr}{/}`);
|
|
589
|
-
lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
590
|
-
} else if (role === 'system') {
|
|
591
|
-
const displayTag = text.startsWith('Action:')
|
|
592
|
-
? '{#88e0b0-fg}Action{/}'
|
|
593
|
-
: text.startsWith('[Code]')
|
|
594
|
-
? '{#ffd166-fg}Code{/}'
|
|
595
|
-
: '{#8ba0ff-fg}System{/}';
|
|
596
|
-
const cleanText = text.replace(/^(Action:|System:)\s*/, '');
|
|
597
|
-
const systemLines = wrapText(cleanText, maxLineWidth - 4);
|
|
598
|
-
chatBox.log(``);
|
|
599
|
-
chatBox.log(` {bold}${displayTag}{/}`);
|
|
600
|
-
systemLines.forEach(l => chatBox.log(` {#95a2b8-fg}${l}{/}`));
|
|
601
|
-
} else if (role === 'error') {
|
|
602
|
-
chatBox.log(``);
|
|
603
|
-
chatBox.log(` {bold}{#ff6b6b-fg}Error{/} {gray-fg}${timeStr}{/}`);
|
|
604
|
-
lines.forEach(l => chatBox.log(` {#7a2e2e-fg}▏{/} {#ff7d7d-fg}${l}{/}`));
|
|
605
|
-
}
|
|
606
|
-
screen.render();
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
/**
|
|
610
|
-
* Opens a streaming message bubble for the assistant.
|
|
611
|
-
* Returns { appendChunk(text), finalize(timestamp) } for typewriter rendering.
|
|
612
|
-
* Usage:
|
|
613
|
-
* const stream = streamMessage('assistant');
|
|
614
|
-
* stream.appendChunk('Hello'); stream.appendChunk(' World');
|
|
615
|
-
* stream.finalize(timestamp);
|
|
616
|
-
*/
|
|
617
|
-
function streamMessage(role = 'assistant') {
|
|
618
|
-
const now = new Date();
|
|
619
|
-
const timeStr = now.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
620
|
-
const maxLineWidth = Math.max(screen.width - 20, 36);
|
|
621
|
-
|
|
622
|
-
// Print the header bubble once
|
|
623
|
-
chatBox.log('');
|
|
624
|
-
if (role === 'assistant') {
|
|
625
|
-
chatBox.log(` {bold}{#d4a8ff-fg}Mint{/} {gray-fg}${timeStr}{/}`);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
let buffer = ''; // accumulates the full response text
|
|
629
|
-
let lineBuffer = ''; // current partial line being built
|
|
630
|
-
let lineRendered = false; // whether we already pushed the first line prefix
|
|
631
|
-
|
|
632
|
-
function flushLine(force = false) {
|
|
633
|
-
// Flush content that fits on one line-width or when forced
|
|
634
|
-
if (!lineBuffer && !force) return;
|
|
635
|
-
if (!lineRendered) {
|
|
636
|
-
chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
|
|
637
|
-
lineRendered = true;
|
|
638
|
-
} else {
|
|
639
|
-
// Overwrite the last line by popping + re-pushing (blessed.log limitation)
|
|
640
|
-
// We can't truly overwrite, so we just keep appending new lines for each chunk.
|
|
641
|
-
// For large chunks, split on newline and emit per-line.
|
|
642
|
-
chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
|
|
643
|
-
}
|
|
644
|
-
screen.render();
|
|
645
|
-
}
|
|
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
|
+
}
|
|
646
186
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
screen.render();
|
|
660
|
-
} else if (lineBuffer.length >= maxLineWidth) {
|
|
661
|
-
// Line overflow — auto-wrap
|
|
662
|
-
const lines = wrapLineSmart(lineBuffer, maxLineWidth);
|
|
663
|
-
lines.slice(0, -1).forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
664
|
-
lineBuffer = lines[lines.length - 1] || '';
|
|
665
|
-
lineRendered = true;
|
|
666
|
-
screen.render();
|
|
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';
|
|
667
199
|
}
|
|
668
|
-
// Otherwise keep buffering the partial line
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function finalize(timestamp = null) {
|
|
673
|
-
// Flush remaining buffer
|
|
674
|
-
if (lineBuffer) {
|
|
675
|
-
const lines = wrapLineSmart(lineBuffer, maxLineWidth);
|
|
676
|
-
lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
677
|
-
lineBuffer = '';
|
|
678
|
-
}
|
|
679
|
-
// Track last response for clipboard
|
|
680
|
-
lastAssistantResponse = buffer;
|
|
681
|
-
screen.render();
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
return { appendChunk, finalize };
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/** Show/hide thinking indicator in status bar */
|
|
688
|
-
function setThinking(active, secondsElapsed = 0) {
|
|
689
|
-
if (active) {
|
|
690
|
-
updateStatusBar(`Thinking... {gray-fg}(esc to cancel, ${secondsElapsed}s){/}`);
|
|
691
|
-
} else {
|
|
692
|
-
updateStatusBar(null);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
/** Copy last assistant response to clipboard */
|
|
697
|
-
function copyLastResponse() {
|
|
698
|
-
if (!lastAssistantResponse) return false;
|
|
699
|
-
return copyToClipboard(lastAssistantResponse);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function requestApproval(request) {
|
|
703
|
-
return new Promise((resolve) => {
|
|
704
|
-
const typeLabel = request.type === 'shell'
|
|
705
|
-
? 'Shell Command'
|
|
706
|
-
: request.type === 'patch'
|
|
707
|
-
? 'Patch Edit'
|
|
708
|
-
: request.type === 'code_mode'
|
|
709
|
-
? 'Enter Code Mode'
|
|
710
|
-
: 'File Write';
|
|
711
|
-
const preview = request.preview || request.label || '';
|
|
712
|
-
const message = [
|
|
713
|
-
`{bold}${typeLabel}{/bold}`,
|
|
714
|
-
'',
|
|
715
|
-
preview,
|
|
716
|
-
'',
|
|
717
|
-
'Approve this action?',
|
|
718
|
-
'', // Extra lines to push buttons down and avoid overlapping
|
|
719
|
-
''
|
|
720
|
-
].join('\n');
|
|
721
|
-
|
|
722
|
-
// Temporarily stop reading input so the dialog can receive keys
|
|
723
|
-
if (inputBox._reading) {
|
|
724
|
-
inputBox.cancel();
|
|
725
|
-
}
|
|
726
200
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
+
});
|
|
736
259
|
|
|
737
|
-
|
|
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
|
+
};
|
|
738
292
|
}
|
|
739
293
|
|
|
740
294
|
module.exports = { createChatUI };
|