@pheem49/mint 1.4.2 → 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.
- package/GUIDE_TH.md +113 -0
- package/README.md +267 -78
- package/assets/CLI_Screen.png +0 -0
- package/main.js +76 -890
- package/mint-cli-logic.js +3 -107
- package/mint-cli.js +594 -29
- package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +10 -0
- 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
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
- 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
- package/package.json +37 -4
- package/src/AI_Brain/Gemini_API.js +223 -65
- package/src/AI_Brain/autonomous_brain.js +11 -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 +354 -10
- package/src/Automation_Layer/file_operations.js +1 -1
- package/src/CLI/chat_router.js +20 -7
- package/src/CLI/chat_ui.js +596 -825
- package/src/CLI/code_agent.js +347 -56
- package/src/CLI/gmail_auth.js +210 -0
- package/src/CLI/image_input.js +90 -0
- package/src/CLI/list_features.js +2 -0
- package/src/CLI/onboarding.js +364 -55
- package/src/CLI/updater.js +210 -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 +178 -0
- package/src/System/bridge_manager.js +76 -0
- package/src/System/chat_history_manager.js +23 -5
- package/src/System/config_manager.js +71 -7
- package/src/System/custom_workflows.js +31 -2
- package/src/System/google_tts_urls.js +51 -0
- package/src/System/granular_automation.js +122 -53
- package/src/System/ipc_handlers.js +238 -0
- package/src/System/proactive_loop.js +153 -0
- package/src/System/safety_manager.js +273 -0
- package/src/System/sandbox_runner.js +182 -0
- package/src/System/screen_capture.js +175 -0
- package/src/System/system_automation.js +127 -81
- package/src/System/system_info.js +70 -0
- package/src/System/task_manager.js +15 -5
- package/src/System/tool_registry.js +280 -0
- package/src/System/window_manager.js +212 -0
- package/src/UI/live2d_manager.js +368 -0
- package/src/UI/renderer.js +208 -24
- package/src/UI/settings.html +24 -0
- package/src/UI/settings.js +14 -4
- package/src/UI/styles.css +466 -32
- package/.codex +0 -0
- package/docs/assets/Agent_Mint.png +0 -0
- package/docs/assets/CLI_Screen.png +0 -0
- package/docs/assets/Settings.png +0 -0
- package/docs/assets/icon.png +0 -0
- package/docs/index.html +0 -132
- package/docs/style.css +0 -579
- package/index.html +0 -16
- package/src/UI/index.html +0 -126
- package/tech_news.txt +0 -3
- package/test_knowledge.txt +0 -3
- package/tests/agent_orchestrator.test.js +0 -41
- package/tests/chat_router.test.js +0 -42
- package/tests/code_agent.test.js +0 -69
- package/tests/config_manager.test.js +0 -141
- package/tests/docker.test.js +0 -46
- package/tests/file_operations.test.js +0 -57
- package/tests/memory_store.test.js +0 -185
- package/tests/provider_routing.test.js +0 -67
- package/tests/spotify.test.js +0 -201
- package/tests/system_monitor.test.js +0 -37
- package/tests/workspace_manager.test.js +0 -56
package/src/CLI/chat_ui.js
CHANGED
|
@@ -1,869 +1,640 @@
|
|
|
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: '/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' },
|
|
19
|
+
{ cmd: '/code', desc: 'Force workspace Code Mode' },
|
|
20
|
+
{ cmd: '/cd', desc: 'Change current working directory' },
|
|
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' },
|
|
24
|
+
{ cmd: '/config', desc: 'Show current configuration' },
|
|
25
|
+
{ cmd: '/copy', desc: 'Copy last response to clipboard' },
|
|
26
|
+
{ cmd: '/clear', desc: 'Clear conversation history' },
|
|
27
|
+
{ cmd: '/reset', desc: 'Reset conversation history' },
|
|
28
|
+
{ cmd: '/agent', desc: 'Switch AI agents (e.g. /agent code)' },
|
|
29
|
+
{ cmd: '/workspace', desc: 'Manage registered workspaces' },
|
|
30
|
+
{ cmd: '/stats', desc: 'Show system statistics' },
|
|
31
|
+
{ cmd: '/review', desc: 'Request second-pass review' },
|
|
32
|
+
{ cmd: '/exit', desc: 'Exit Mint' }
|
|
23
33
|
];
|
|
24
34
|
|
|
25
35
|
/**
|
|
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 }}
|
|
36
|
+
* We wrap everything in an async function to load ESM modules
|
|
31
37
|
*/
|
|
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
|
-
|
|
38
|
+
async function createChatUI(options) {
|
|
39
|
+
// Dynamic imports for ESM modules
|
|
40
|
+
const { render, Box, Text, useInput, useApp, Static } = await import('ink');
|
|
41
|
+
const TextInput = (await import('ink-text-input')).default;
|
|
42
|
+
const { useState, useImperativeHandle, forwardRef, createRef, useEffect, useMemo } = React;
|
|
43
|
+
|
|
44
|
+
const App = forwardRef(({ onSubmit, onExit, onPasteImage, initialHistory = [] }, ref) => {
|
|
45
|
+
const config = readConfig();
|
|
46
|
+
const { exit } = useApp();
|
|
47
|
+
const [input, setInput] = useState('');
|
|
48
|
+
const [history, setHistory] = useState(initialHistory);
|
|
49
|
+
const [liveAssistant, setLiveAssistant] = useState(null);
|
|
50
|
+
const [thinking, setThinking] = useState(false);
|
|
51
|
+
const [fastMode, setFastMode] = useState(false);
|
|
52
|
+
const [mode, setMode] = useState('Agent');
|
|
53
|
+
const [model, setModel] = useState('');
|
|
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');
|
|
61
|
+
|
|
62
|
+
// Suggestions State
|
|
63
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
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);
|
|
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
|
+
};
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
inputRef.current = input;
|
|
92
|
+
}, [input]);
|
|
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
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
selectedIndexRef.current = selectedIndex;
|
|
120
|
+
}, [selectedIndex]);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
pendingApprovalRef.current = pendingApproval;
|
|
124
|
+
if (pendingApproval) {
|
|
125
|
+
approvalChoiceRef.current = 'approve';
|
|
126
|
+
setApprovalChoice('approve');
|
|
106
127
|
}
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
}, [pendingApproval]);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
approvalChoiceRef.current = approvalChoice;
|
|
132
|
+
}, [approvalChoice]);
|
|
133
|
+
|
|
134
|
+
const showSuggestions = input.startsWith('/') && !input.includes(' ');
|
|
135
|
+
const suggestions = useMemo(() => {
|
|
136
|
+
if (!showSuggestions) return [];
|
|
137
|
+
const query = input.toLowerCase();
|
|
138
|
+
return SLASH_COMMANDS.filter(s => s.cmd.startsWith(query));
|
|
139
|
+
}, [input, showSuggestions]);
|
|
140
|
+
|
|
141
|
+
// Reset index when suggestions change
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
setSelectedIndex(0);
|
|
144
|
+
}, [suggestions.length]);
|
|
145
|
+
|
|
146
|
+
const lastSystemMessage = React.useRef('');
|
|
147
|
+
|
|
148
|
+
// Export methods to the outside world via ref
|
|
149
|
+
useImperativeHandle(ref, () => ({
|
|
150
|
+
appendMessage: (role, text, metadata = {}) => {
|
|
151
|
+
setHistory(prev => [...prev, { role, text, time: new Date(), ...metadata }]);
|
|
152
|
+
if (metadata.providerInfo) {
|
|
153
|
+
const { provider, model } = metadata.providerInfo;
|
|
154
|
+
setModel(model ? `${provider} • ${model}` : provider);
|
|
155
|
+
}
|
|
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
|
+
},
|
|
180
|
+
setThinking: (val) => setThinking(val),
|
|
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
|
+
},
|
|
202
|
+
updateStatusModel: (val) => setModel(val),
|
|
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
|
+
},
|
|
215
|
+
appendCodeStep: (info) => {
|
|
216
|
+
if (fastModeRef.current) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
121
219
|
|
|
220
|
+
let text = '';
|
|
221
|
+
let label = 'System';
|
|
222
|
+
let labelColor = 'blueBright';
|
|
223
|
+
let isThought = false;
|
|
224
|
+
|
|
225
|
+
if (typeof info === 'string') {
|
|
226
|
+
text = info;
|
|
227
|
+
} else {
|
|
228
|
+
const { action, phase, target, message, thought } = info;
|
|
229
|
+
if (action === 'memory_context' && process.env.MINT_SHOW_MEMORY_TRACE !== '1') {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (thought) {
|
|
233
|
+
text = thought;
|
|
234
|
+
label = 'Thinking';
|
|
235
|
+
labelColor = 'gray';
|
|
236
|
+
isThought = true;
|
|
237
|
+
} else if (action === 'thinking' || phase === 'thinking') {
|
|
238
|
+
return;
|
|
239
|
+
} else {
|
|
240
|
+
label = action || phase || 'Action';
|
|
241
|
+
text = target || message || '';
|
|
242
|
+
if (!text) return;
|
|
243
|
+
|
|
244
|
+
// Color coding for specific actions
|
|
245
|
+
if (label.includes('search')) labelColor = 'yellowBright';
|
|
246
|
+
else if (label.includes('file') || label.includes('path')) labelColor = 'cyanBright';
|
|
247
|
+
else if (label.includes('write') || label.includes('edit') || label.includes('patch')) labelColor = 'greenBright';
|
|
248
|
+
else if (label.includes('shell') || label.includes('run')) labelColor = 'magentaBright';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
122
251
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
252
|
+
const fullText = `[${label}] ${text}`;
|
|
253
|
+
if (fullText === lastSystemMessage.current) return;
|
|
254
|
+
lastSystemMessage.current = fullText;
|
|
255
|
+
|
|
256
|
+
setHistory(prev => [...prev, {
|
|
257
|
+
role: 'system',
|
|
258
|
+
label,
|
|
259
|
+
labelColor,
|
|
260
|
+
text,
|
|
261
|
+
isThought,
|
|
262
|
+
time: new Date()
|
|
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
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
// Handle exiting and keyboard navigation
|
|
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
|
+
}
|
|
134
302
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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;
|
|
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;
|
|
165
315
|
}
|
|
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
316
|
return;
|
|
175
317
|
}
|
|
176
|
-
const attr = inputBox.sattr(inputBox.style);
|
|
177
|
-
screen.program.write(screen.codeAttr(attr));
|
|
178
|
-
} catch (_) {}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ─── Status bar (2 lines as per screenshot) ──────────────────────
|
|
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
|
-
// 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
318
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
319
|
+
if (key.escape && pendingImagesRef.current.length > 0) {
|
|
320
|
+
setPendingImages([]);
|
|
321
|
+
pendingImagesRef.current = [];
|
|
322
|
+
setPendingImagePrefix('');
|
|
323
|
+
pendingImagePrefixRef.current = '';
|
|
324
|
+
return;
|
|
298
325
|
}
|
|
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
326
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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();
|
|
327
|
+
if (key.escape && pendingPasteRef.current) {
|
|
328
|
+
setPendingPaste(null);
|
|
329
|
+
pendingPasteRef.current = null;
|
|
330
|
+
setPendingPastePrefix('');
|
|
331
|
+
pendingPastePrefixRef.current = '';
|
|
332
|
+
return;
|
|
389
333
|
}
|
|
390
334
|
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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;
|
|
395
343
|
}
|
|
396
|
-
});
|
|
397
|
-
});
|
|
398
344
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
345
|
+
if (key.escape || (key.ctrl && inputStr === 'c')) {
|
|
346
|
+
exit();
|
|
347
|
+
onExit();
|
|
348
|
+
return;
|
|
435
349
|
}
|
|
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
350
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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 = '';
|
|
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
|
+
});
|
|
563
385
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const currentShowSuggestions = currentInput.startsWith('/') && !currentInput.includes(' ');
|
|
390
|
+
|
|
391
|
+
if (currentShowSuggestions) {
|
|
392
|
+
const query = currentInput.toLowerCase();
|
|
393
|
+
const currentSuggestions = SLASH_COMMANDS.filter(s => s.cmd.startsWith(query));
|
|
394
|
+
|
|
395
|
+
if (currentSuggestions.length > 0) {
|
|
396
|
+
if (key.upArrow) {
|
|
397
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : currentSuggestions.length - 1));
|
|
398
|
+
} else if (key.downArrow) {
|
|
399
|
+
setSelectedIndex(prev => (prev < currentSuggestions.length - 1 ? prev + 1 : 0));
|
|
400
|
+
} else if (key.tab || (key.return && currentInput.startsWith('/'))) {
|
|
401
|
+
const picked = currentSuggestions[selectedIndexRef.current];
|
|
402
|
+
if (picked) {
|
|
403
|
+
setInput(picked.cmd + ' ');
|
|
404
|
+
}
|
|
570
405
|
}
|
|
571
406
|
}
|
|
572
|
-
continue;
|
|
573
407
|
}
|
|
408
|
+
});
|
|
574
409
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
410
|
+
const handleSubmit = (value) => {
|
|
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) {
|
|
425
|
+
const picked = suggestions[selectedIndex];
|
|
426
|
+
if (picked && text !== picked.cmd) {
|
|
427
|
+
setInput(picked.cmd + ' ');
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
578
430
|
}
|
|
579
431
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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}{/}`);
|
|
432
|
+
setInput('');
|
|
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 });
|
|
442
|
+
};
|
|
443
|
+
|
|
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;
|
|
672
452
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
lineBuffer += segments[i];
|
|
682
|
-
if (i < segments.length - 1) {
|
|
683
|
-
// Newline boundary — emit current line
|
|
684
|
-
const lines = wrapLineSmart(lineBuffer, maxLineWidth);
|
|
685
|
-
lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
686
|
-
lineBuffer = '';
|
|
687
|
-
lineRendered = true;
|
|
688
|
-
screen.render();
|
|
689
|
-
} else if (lineBuffer.length >= maxLineWidth) {
|
|
690
|
-
// Line overflow — auto-wrap
|
|
691
|
-
const lines = wrapLineSmart(lineBuffer, maxLineWidth);
|
|
692
|
-
lines.slice(0, -1).forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
693
|
-
lineBuffer = lines[lines.length - 1] || '';
|
|
694
|
-
lineRendered = true;
|
|
695
|
-
screen.render();
|
|
453
|
+
|
|
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;
|
|
696
461
|
}
|
|
697
|
-
|
|
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}{/}`));
|
|
462
|
+
if (normalizedValue.length > previous.length && /^[vV]$/.test(normalizedValue.slice(previous.length))) {
|
|
463
|
+
setInput(previous);
|
|
464
|
+
return;
|
|
750
465
|
}
|
|
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
466
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function askUser(question) {
|
|
829
|
-
return new Promise((resolve) => {
|
|
830
|
-
// Temporarily stop reading input so we can capture the answer
|
|
831
|
-
if (inputBox._reading) {
|
|
832
|
-
inputBox.cancel();
|
|
467
|
+
setInput(normalizedValue);
|
|
468
|
+
};
|
|
469
|
+
|
|
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}`)
|
|
474
|
+
);
|
|
833
475
|
}
|
|
834
476
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const submitHandler = (value) => {
|
|
849
|
-
inputBox.removeListener('submit', submitHandler);
|
|
850
|
-
inputBox._label.setContent(oldLabel);
|
|
851
|
-
|
|
852
|
-
const answer = value || '';
|
|
853
|
-
chatBox.log('');
|
|
854
|
-
chatBox.log(` {bold}User answered:{/}`);
|
|
855
|
-
const lines = wrapText(answer, screen.width - 20);
|
|
856
|
-
lines.forEach(l => chatBox.log(` {#95a2b8-fg}${l}{/}`));
|
|
857
|
-
screen.render();
|
|
858
|
-
|
|
859
|
-
resolve(answer);
|
|
860
|
-
};
|
|
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
|
+
}
|
|
861
490
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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'),
|
|
506
|
+
|
|
507
|
+
// Floating (Persistent) UI part
|
|
508
|
+
h(Box, { flexDirection: 'column' },
|
|
509
|
+
thinking && h(Box, { marginBottom: 1 },
|
|
510
|
+
h(Text, { color: 'yellow' }, '● Mint is thinking...')
|
|
511
|
+
),
|
|
512
|
+
|
|
513
|
+
// Suggestions Menu
|
|
514
|
+
showSuggestions && suggestions.length > 0 && h(Box, {
|
|
515
|
+
flexDirection: 'column',
|
|
516
|
+
borderStyle: 'single',
|
|
517
|
+
borderColor: 'gray',
|
|
518
|
+
paddingX: 1,
|
|
519
|
+
marginBottom: 0
|
|
520
|
+
},
|
|
521
|
+
suggestions.map((s, i) => h(Box, { key: s.cmd, flexDirection: 'row' },
|
|
522
|
+
h(Text, {
|
|
523
|
+
backgroundColor: i === selectedIndex ? 'green' : undefined,
|
|
524
|
+
color: i === selectedIndex ? 'white' : 'greenBright'
|
|
525
|
+
}, s.cmd.padEnd(12)),
|
|
526
|
+
h(Text, { color: 'gray' }, ` ${s.desc}`)
|
|
527
|
+
))
|
|
528
|
+
),
|
|
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
|
+
|
|
561
|
+
// Compact Input Area
|
|
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
|
+
)
|
|
582
|
+
),
|
|
583
|
+
|
|
584
|
+
// Status Bar
|
|
585
|
+
h(Box, { justifyContent: 'space-between' },
|
|
586
|
+
h(Box, null,
|
|
587
|
+
h(Text, { color: 'cyan' }, `[${fastMode ? 'Fast' : mode}] `),
|
|
588
|
+
h(Text, { color: 'magentaBright' }, (model || config.geminiModel || 'gemini').slice(0, 46))
|
|
589
|
+
),
|
|
590
|
+
h(Box, null,
|
|
591
|
+
h(Text, { color: 'gray' }, `path: ...${workspace.slice(-20)}`)
|
|
592
|
+
)
|
|
593
|
+
)
|
|
594
|
+
)
|
|
595
|
+
);
|
|
596
|
+
});
|
|
865
597
|
|
|
866
|
-
|
|
598
|
+
// Print banner once before rendering the main app-
|
|
599
|
+
console.log(`\x1b[38;5;121m\x1b[1m __ __ _ _ ___ _ ___ \x1b[0m`);
|
|
600
|
+
console.log(`\x1b[38;5;121m\x1b[1m| \\/ (_)_ __ | |_ / __| | |_ _|\x1b[0m`);
|
|
601
|
+
console.log(`\x1b[38;5;121m\x1b[1m| |\\/| | | '_ \\| _| (__| |__ | | \x1b[0m`);
|
|
602
|
+
console.log(`\x1b[38;5;121m\x1b[1m|_| |_|_|_| |_|\\__|\\___|____|___|\x1b[0m`);
|
|
603
|
+
console.log(`\x1b[90mType naturally to chat. Esc to exit.\x1b[0m\n`);
|
|
604
|
+
|
|
605
|
+
const ref = createRef();
|
|
606
|
+
render(h(App, { ref, ...options }), { exitOnCtrlC: false });
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
appendMessage: (role, text, metadata) => ref.current?.appendMessage(role, text, metadata),
|
|
610
|
+
setThinking: (val) => ref.current?.setThinking(val),
|
|
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),
|
|
617
|
+
updateStatusModel: (val) => ref.current?.updateStatusModel(val),
|
|
618
|
+
updateWorkspace: (val) => ref.current?.updateWorkspace(val),
|
|
619
|
+
attachImage: (image) => ref.current?.attachImage(image),
|
|
620
|
+
appendCodeStep: (info) => ref.current?.appendCodeStep(info),
|
|
621
|
+
streamMessage: (metadata = {}) => {
|
|
622
|
+
let fullText = '';
|
|
623
|
+
ref.current?.beginAssistantStream(metadata);
|
|
624
|
+
return {
|
|
625
|
+
appendChunk: (chunk) => {
|
|
626
|
+
fullText += chunk;
|
|
627
|
+
ref.current?.appendAssistantStreamChunk(chunk);
|
|
628
|
+
},
|
|
629
|
+
finalize: () => {
|
|
630
|
+
ref.current?.finalizeAssistantStream();
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
},
|
|
634
|
+
copyLastResponse: () => false,
|
|
635
|
+
requestApproval: (request) => ref.current?.requestApproval(request) || Promise.resolve(false),
|
|
636
|
+
askUser: () => Promise.resolve('')
|
|
637
|
+
};
|
|
867
638
|
}
|
|
868
639
|
|
|
869
640
|
module.exports = { createChatUI };
|