@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.
Files changed (97) hide show
  1. package/GUIDE_TH.md +113 -0
  2. package/README.md +267 -78
  3. package/assets/CLI_Screen.png +0 -0
  4. package/main.js +76 -890
  5. package/mint-cli-logic.js +3 -107
  6. package/mint-cli.js +594 -29
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
  9. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +10 -0
  10. 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
  11. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
  12. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
  13. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
  14. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +10 -0
  15. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
  16. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
  17. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
  18. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
  19. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
  20. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
  21. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
  22. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
  23. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
  24. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
  25. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
  26. 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
  27. package/package.json +37 -4
  28. package/src/AI_Brain/Gemini_API.js +223 -65
  29. package/src/AI_Brain/autonomous_brain.js +11 -0
  30. package/src/AI_Brain/behavior_memory.js +26 -5
  31. package/src/AI_Brain/headless_agent.js +4 -0
  32. package/src/AI_Brain/knowledge_base.js +61 -8
  33. package/src/AI_Brain/memory_store.js +354 -10
  34. package/src/Automation_Layer/file_operations.js +1 -1
  35. package/src/CLI/chat_router.js +20 -7
  36. package/src/CLI/chat_ui.js +596 -825
  37. package/src/CLI/code_agent.js +347 -56
  38. package/src/CLI/gmail_auth.js +210 -0
  39. package/src/CLI/image_input.js +90 -0
  40. package/src/CLI/list_features.js +2 -0
  41. package/src/CLI/onboarding.js +364 -55
  42. package/src/CLI/updater.js +210 -0
  43. package/src/Channels/brave_search_bridge.js +35 -0
  44. package/src/Channels/discord_bridge.js +68 -0
  45. package/src/Channels/google_search_bridge.js +38 -0
  46. package/src/Channels/line_bridge.js +60 -0
  47. package/src/Channels/slack_bridge.js +53 -0
  48. package/src/Channels/telegram_bridge.js +49 -0
  49. package/src/Channels/whatsapp_bridge.js +55 -0
  50. package/src/Command_Parser/parser.js +12 -1
  51. package/src/Plugins/gmail.js +251 -0
  52. package/src/Plugins/google_calendar.js +245 -19
  53. package/src/Plugins/notion.js +256 -0
  54. package/src/System/action_executor.js +178 -0
  55. package/src/System/bridge_manager.js +76 -0
  56. package/src/System/chat_history_manager.js +23 -5
  57. package/src/System/config_manager.js +71 -7
  58. package/src/System/custom_workflows.js +31 -2
  59. package/src/System/google_tts_urls.js +51 -0
  60. package/src/System/granular_automation.js +122 -53
  61. package/src/System/ipc_handlers.js +238 -0
  62. package/src/System/proactive_loop.js +153 -0
  63. package/src/System/safety_manager.js +273 -0
  64. package/src/System/sandbox_runner.js +182 -0
  65. package/src/System/screen_capture.js +175 -0
  66. package/src/System/system_automation.js +127 -81
  67. package/src/System/system_info.js +70 -0
  68. package/src/System/task_manager.js +15 -5
  69. package/src/System/tool_registry.js +280 -0
  70. package/src/System/window_manager.js +212 -0
  71. package/src/UI/live2d_manager.js +368 -0
  72. package/src/UI/renderer.js +208 -24
  73. package/src/UI/settings.html +24 -0
  74. package/src/UI/settings.js +14 -4
  75. package/src/UI/styles.css +466 -32
  76. package/.codex +0 -0
  77. package/docs/assets/Agent_Mint.png +0 -0
  78. package/docs/assets/CLI_Screen.png +0 -0
  79. package/docs/assets/Settings.png +0 -0
  80. package/docs/assets/icon.png +0 -0
  81. package/docs/index.html +0 -132
  82. package/docs/style.css +0 -579
  83. package/index.html +0 -16
  84. package/src/UI/index.html +0 -126
  85. package/tech_news.txt +0 -3
  86. package/test_knowledge.txt +0 -3
  87. package/tests/agent_orchestrator.test.js +0 -41
  88. package/tests/chat_router.test.js +0 -42
  89. package/tests/code_agent.test.js +0 -69
  90. package/tests/config_manager.test.js +0 -141
  91. package/tests/docker.test.js +0 -46
  92. package/tests/file_operations.test.js +0 -57
  93. package/tests/memory_store.test.js +0 -185
  94. package/tests/provider_routing.test.js +0 -67
  95. package/tests/spotify.test.js +0 -201
  96. package/tests/system_monitor.test.js +0 -37
  97. package/tests/workspace_manager.test.js +0 -56
@@ -1,869 +1,640 @@
1
1
  /**
2
- * Mint CLI - Gemini-style TUI using blessed
3
- * Provides a rich terminal UI with chat history, input box, and status bar
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 blessed = require('blessed');
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
- { name: '/code', desc: 'Force workspace code mode for a task' },
12
- { name: '/models', desc: 'List or switch Gemini models' },
13
- { name: '/config', desc: 'Show current configuration' },
14
- { name: '/copy', desc: 'Copy last response to clipboard' },
15
- { name: '/clear', desc: 'Clear conversation history' },
16
- { name: '/reset', desc: 'Reset conversation history' },
17
- { name: '/agent', desc: 'Switch AI personas (coder, researcher, etc)' },
18
- { name: '/workspace', desc: 'Manage project-specific contexts' },
19
- { name: '/review', desc: 'Request a second-pass review of the last response' },
20
- { name: '/stats', desc: 'Show system health stats (CPU/RAM/Disk)' },
21
- { name: '/help', desc: 'Show help information' },
22
- { name: '/exit', desc: 'Exit Mint' }
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
- * Creates and returns the Mint chat TUI screen
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({ onSubmit, onExit }) {
33
- const config = readConfig();
34
- const modelName = config.geminiModel || 'gemini';
35
- const workspacePath = process.cwd();
36
- const HINT_DEFAULT = `{gray-fg} Enter send · Ctrl+Y copy · /help commands{/}`;
37
- const INPUT_FG = '#f8fafc';
38
- const INPUT_BG = '#10141c';
39
-
40
- // ─── Screen ───────────────────────────────────────────────────────────────
41
- const screen = blessed.screen({
42
- smartCSR: true,
43
- fullUnicode: true,
44
- title: 'Mint CLI',
45
- mouse: true
46
- });
47
-
48
- // ─── Banner ───────────────────────────────────────────────────────────────
49
- const banner = blessed.box({
50
- top: 0, left: 1, width: '100%-2', height: 4,
51
- tags: true,
52
- padding: { left: 1, right: 1 },
53
- style: { bg: 'default', fg: '#d7dde8' }
54
- });
55
- banner.setContent([
56
- `{#88e0b0-fg} __ __ _ _ ___ _ ___ {/}`,
57
- `{#88e0b0-fg}| \\/ (_)_ __ | |_ / __| | |_ _|{/}`,
58
- `{#88e0b0-fg}| |\\/| | | '_ \\| _| (__| |__ | | {/}`,
59
- `{#88e0b0-fg}|_| |_|_|_| |_|\\__|\\___|____|___|{/}`
60
- ].join('\n'));
61
-
62
- const subBanner = blessed.box({
63
- top: 4, left: 2, width: '100%-4', height: 2,
64
- tags: true,
65
- content: `{gray-fg}Type naturally to chat. Coding requests can auto-enter {/}{#ffd166-fg}Code Mode{/}{gray-fg}. Use {/}{#88e0b0-fg}/help{/}{gray-fg}, {/}{#88e0b0-fg}/code{/}{gray-fg}, or {/}{#88e0b0-fg}Esc{/}{gray-fg}.{/}`,
66
- style: { bg: 'default', fg: '#9aa6bf' }
67
- });
68
-
69
- // ─── Chat log (scrollable) ────────────────────────────────────────────────
70
- const chatBox = blessed.log({
71
- top: 6, left: 1, width: '100%-2',
72
- bottom: 8,
73
- tags: true,
74
- scrollable: true,
75
- alwaysScroll: true,
76
- scrollbar: { ch: '', style: { fg: '#335d52' } },
77
- style: { bg: '#171b24', fg: '#ffffff', border: { fg: '#2f3747' } },
78
- mouse: true,
79
- scrollable: true,
80
- border: { type: 'line' },
81
- padding: { left: 1, right: 1, top: 0, bottom: 0 },
82
- label: ' Conversation '
83
- });
84
-
85
- // ─── Hint bar ─────────────────────────────────────────────────────────────
86
- const hintBar = blessed.box({
87
- bottom: 6, left: 1, width: '100%-2', height: 1,
88
- tags: true,
89
- content: HINT_DEFAULT,
90
- style: { bg: 'default' }
91
- });
92
-
93
- const inputBox = blessed.textbox({
94
- bottom: 3, left: 1, width: '100%-2', height: 3,
95
- tags: false,
96
- inputOnFocus: false, // We'll manage this manually for stability
97
- keys: true,
98
- style: {
99
- bg: INPUT_BG,
100
- fg: INPUT_FG,
101
- border: { fg: '#335d52' },
102
- focus: {
103
- fg: INPUT_FG,
104
- bg: INPUT_BG,
105
- border: { fg: '#88e0b0' }
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
- border: { type: 'line' },
109
- padding: { left: 1 },
110
- label: ' Message '
111
- });
112
-
113
- // --- SAFETY PATCH ---
114
- // Prevent "TypeError: done is not a function" if a listener survives a blur/focus cycle.
115
- const originalListener = inputBox._listener;
116
- inputBox._listener = function(ch, key) {
117
- if (typeof this._done !== 'function') return;
118
- return originalListener.call(this, ch, key);
119
- };
120
-
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
- // ─── 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
- });
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
- 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;
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
- let activeMode = 'Chat';
228
- let spinnerIdx = 0;
229
- const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
230
-
231
- function formatTime(seconds) {
232
- if (seconds < 60) return `${seconds}s`;
233
- const mins = Math.floor(seconds / 60);
234
- const secs = seconds % 60;
235
- return `${mins}m ${secs}s`;
236
- }
237
-
238
- function updateStatusBar(thinkingText = null, secondsElapsed = 0) {
239
- if (thinkingText) {
240
- const char = spinnerChars[spinnerIdx % spinnerChars.length];
241
- spinnerIdx++;
242
- statusLine1.setContent(`{#88e0b0-fg}${char}{/} Thinking... {gray-fg}(esc to cancel, ${formatTime(secondsElapsed)}){/}`);
243
- } else {
244
- statusLine1.setContent(`${activeMode === 'Code' ? '{#ffd166-fg}[Code]{/}' : '{#88aaff-fg}[Chat]{/}'} {#cc4444-fg}no sandbox{/}`);
245
- }
246
- screen.render();
247
- }
248
-
249
- function setMode(mode) {
250
- activeMode = mode === 'Code' ? 'Code' : 'Chat';
251
- updateStatusBar(null);
252
- }
253
-
254
- function updateStatusModel(newModel) {
255
- if (!newModel) return;
256
- shortcutHint.setContent(`{gray-fg}${newModel} · ? for shortcuts{/}`);
257
- screen.render();
258
- }
259
-
260
- /** Update workspace name in status bar */
261
- function updateWorkspace(newPath) {
262
- if (!newPath) return;
263
- fileInfo.setContent(`{gray-fg}path: ${newPath}{/}`);
264
- screen.render();
265
- }
266
- updateStatusBar();
267
-
268
- // ─── Append widgets to screen ─────────────────────────────────────────────
269
- screen.append(banner);
270
- screen.append(subBanner);
271
- screen.append(chatBox);
272
- screen.append(hintBar);
273
- screen.append(inputBox);
274
- screen.append(statusBar);
275
- screen.append(placeholderWidget); // sibling on top of inputBox
276
-
277
- // Suggestion List and Approval Dialog remain same ...
278
-
279
- // ─── Suggestion List ──────────────────────────────────────────────────────
280
- const commandList = blessed.list({
281
- parent: screen,
282
- bottom: 6,
283
- left: 3,
284
- width: '64%',
285
- height: 8,
286
- tags: true,
287
- keys: false, // We will handle keys manually to keep focus on input
288
- vi: false,
289
- hidden: true,
290
- border: { type: 'line', fg: '#88e0b0' },
291
- style: {
292
- bg: '#10141c',
293
- fg: '#ffffff',
294
- selected: {
295
- bg: '#22352f',
296
- fg: '#88e0b0',
297
- bold: true
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
- function updateSuggestions(filter = '') {
321
- activeSuggestions = SLASH_COMMANDS.filter(cmd =>
322
- cmd.name.toLowerCase().startsWith(filter.toLowerCase())
323
- );
324
-
325
- if (activeSuggestions.length === 0) {
326
- commandList.hide();
327
- screen.render();
328
- return;
329
- }
330
-
331
- const items = activeSuggestions.map(cmd =>
332
- ` {bold}${cmd.name}{/} {gray-fg}${cmd.desc}{/}`
333
- );
334
- commandList.setItems(items);
335
- commandList.select(0);
336
- commandList.show();
337
- commandList.setFront();
338
- screen.render();
339
- }
340
-
341
-
342
- // ─── Input events ─────────────────────────────────────────────────────────
343
-
344
- // ─── Input events ─────────────────────────────────────────────────────────
345
- let lastListVisible = false;
346
-
347
- // Consolidated key handling
348
- inputBox.on('element keypress', (el, ch, key) => {
349
- refreshInputStyles();
350
- // 1. Handle placeholder visibility
351
- if (!key.ctrl && !key.meta && key.name !== 'enter' && key.name !== 'tab') {
352
- if (ch) hidePlaceholder();
353
- }
354
-
355
- // 2. Handle suggestion list navigation
356
- if (!commandList.hidden) {
357
- if (key.name === 'up') {
358
- commandList.up();
359
- screen.render();
360
- return false;
361
- }
362
- if (key.name === 'down') {
363
- commandList.down();
364
- screen.render();
365
- return false;
366
- }
367
- if (key.name === 'escape') {
368
- commandList.hide();
369
- lastListVisible = false;
370
- screen.render();
371
- return false;
372
- }
373
- }
374
-
375
- // 3. Logic for suggestions and placeholder after key is processed
376
- setImmediate(() => {
377
- refreshInputStyles();
378
- const val = (inputBox.getValue ? inputBox.getValue() : inputBox.value) || '';
379
- const isCommand = val.startsWith('/') && !val.includes(' ');
380
-
381
- // Only render if visibility changed or list is updated
382
- if (isCommand) {
383
- updateSuggestions(val);
384
- lastListVisible = true;
385
- } else if (lastListVisible) {
386
- commandList.hide();
387
- lastListVisible = false;
388
- screen.render();
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 (!val.trim()) {
392
- showPlaceholder();
393
- } else {
394
- hidePlaceholder();
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
- inputBox.on('focus', () => {
400
- refreshInputStyles();
401
- screen.render();
402
- });
403
-
404
- inputBox.on('keypress', () => {
405
- applyTerminalInputAttrs();
406
- });
407
-
408
- // Restore focus to inputBox when clicked or when screen is clicked
409
- screen.on('click', () => {
410
- if (!approvalDialog.visible) {
411
- inputBox.focus();
412
- screen.render();
413
- }
414
- });
415
-
416
- inputBox.on('click', () => {
417
- inputBox.focus();
418
- screen.render();
419
- });
420
-
421
-
422
- // Submit or Select Suggestion on Enter
423
- inputBox.on('submit', (value) => {
424
- if (!commandList.hidden) {
425
- const selected = activeSuggestions[commandList.selected];
426
- if (selected) {
427
- inputBox.setValue(selected.name + ' ');
428
- commandList.hide();
429
- hidePlaceholder();
430
- inputBox.focus();
431
- inputBox.readInput(); // Re-focus to continue typing
432
- refreshInputStyles();
433
- screen.render();
434
- return; // Don't submit yet, let user add args or press enter again
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
- // ESC exit immediately
487
- screen.key(['escape'], () => {
488
- onExit();
489
- });
490
-
491
- // ─── Clipboard copy (Ctrl+Y) ──────────────────────────────────────────────
492
- function copyToClipboard(text) {
493
- // Try xclip first, then xsel as fallback
494
- const tools = [
495
- `echo ${JSON.stringify(text)} | xclip -selection clipboard`,
496
- `echo ${JSON.stringify(text)} | xsel --clipboard --input`
497
- ];
498
- for (const cmd of tools) {
499
- try {
500
- execSync(cmd, { stdio: 'pipe' });
501
- return true;
502
- } catch (_) {}
503
- }
504
- return false;
505
- }
506
-
507
- function flashHint(msg, durationMs = 2000) {
508
- hintBar.setContent(msg);
509
- screen.render();
510
- setTimeout(() => {
511
- hintBar.setContent(HINT_DEFAULT);
512
- screen.render();
513
- }, durationMs);
514
- }
515
-
516
- screen.key(['C-y'], () => {
517
- if (!lastAssistantResponse) {
518
- flashHint(`{yellow-fg} No response to copy yet.{/}`);
519
- return;
520
- }
521
- const ok = copyToClipboard(lastAssistantResponse);
522
- if (ok) {
523
- flashHint(`{#88e0b0-fg} ✓ Copied to clipboard!{/}`);
524
- } else {
525
- flashHint(`{red-fg} ✖ Copy failed. Install xclip: sudo apt install xclip{/}`, 3000);
526
- }
527
- });
528
-
529
- // ─── Initial render ───────────────────────────────────────────────────────
530
- inputBox.focus();
531
- inputBox.readInput(); // Initial start
532
- refreshInputStyles();
533
- screen.render();
534
-
535
- // ─── Public API ───────────────────────────────────────────────────────────
536
-
537
- // Track last assistant response for clipboard copy
538
- let lastAssistantResponse = '';
539
-
540
- /**
541
- * @param {'user'|'assistant'|'system'|'error'} role
542
- * @param {string} text
543
- * @param {string} timestamp - ISO string or Date object
544
- */
545
- function wrapLineSmart(line, width) {
546
- if (line.length <= width) return [line];
547
- if (!line.includes(' ')) {
548
- const pieces = [];
549
- for (let index = 0; index < line.length; index += width) {
550
- pieces.push(line.slice(index, index + width));
551
- }
552
- return pieces;
553
- }
554
-
555
- const words = line.split(/\s+/);
556
- const lines = [];
557
- let current = '';
558
- for (const word of words) {
559
- if (word.length > width) {
560
- if (current) {
561
- lines.push(current);
562
- current = '';
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
- for (let index = 0; index < word.length; index += width) {
565
- const slice = word.slice(index, index + width);
566
- if (slice.length === width) {
567
- lines.push(slice);
568
- } else {
569
- current = slice;
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
- if (!current) {
576
- current = word;
577
- continue;
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
- if (`${current} ${word}`.length <= width) {
581
- current += ` ${word}`;
582
- } else {
583
- lines.push(current);
584
- current = word;
585
- }
586
- }
587
- if (current) lines.push(current);
588
- return lines;
589
- }
590
-
591
- function wrapText(str, width) {
592
- const lines = [];
593
- const originalLines = String(str).split('\n');
594
- for (const line of originalLines) {
595
- if (line.length === 0) {
596
- lines.push('');
597
- continue;
598
- }
599
- lines.push(...wrapLineSmart(line, width));
600
- }
601
- return lines;
602
- }
603
-
604
- function appendMessage(role, text, timestamp = null) {
605
- const now = timestamp ? new Date(timestamp) : new Date();
606
- const timeStr = now.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
607
- const maxLineWidth = Math.max(screen.width - 20, 36);
608
- const lines = wrapText(text, maxLineWidth);
609
-
610
- if (role === 'user') {
611
- chatBox.log(``);
612
- chatBox.log(` {bold}{#88e0b0-fg}You{/} {gray-fg}${timeStr}{/}`);
613
- lines.forEach(l => chatBox.log(` {#88e0b0-fg}▏{/} {#ffffff-fg}${l}{/}`));
614
- } else if (role === 'assistant') {
615
- lastAssistantResponse = text;
616
- chatBox.log(``);
617
- chatBox.log(` {bold}{#d4a8ff-fg}Mint{/} {gray-fg}${timeStr}{/}`);
618
- lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
619
- } else if (role === 'system') {
620
- const displayTag = text.startsWith('Action:')
621
- ? '{#88e0b0-fg}Action{/}'
622
- : text.startsWith('[Code]')
623
- ? '{#ffd166-fg}Code{/}'
624
- : '{#8ba0ff-fg}System{/}';
625
- const cleanText = text.replace(/^(Action:|System:)\s*/, '');
626
- const systemLines = wrapText(cleanText, maxLineWidth - 4);
627
- chatBox.log(``);
628
- chatBox.log(` {bold}${displayTag}{/}`);
629
- systemLines.forEach(l => chatBox.log(` {#95a2b8-fg}${l}{/}`));
630
- } else if (role === 'error') {
631
- chatBox.log(``);
632
- chatBox.log(` {bold}{#ff6b6b-fg}Error{/} {gray-fg}${timeStr}{/}`);
633
- lines.forEach(l => chatBox.log(` {#7a2e2e-fg}▏{/} {#ff7d7d-fg}${l}{/}`));
634
- }
635
- screen.render();
636
- }
637
-
638
- /**
639
- * Opens a streaming message bubble for the assistant.
640
- * Returns { appendChunk(text), finalize(timestamp) } for typewriter rendering.
641
- * Usage:
642
- * const stream = streamMessage('assistant');
643
- * stream.appendChunk('Hello'); stream.appendChunk(' World');
644
- * stream.finalize(timestamp);
645
- */
646
- function streamMessage(role = 'assistant') {
647
- const now = new Date();
648
- const timeStr = now.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
649
- const maxLineWidth = Math.max(screen.width - 20, 36);
650
-
651
- // Print the header bubble once
652
- chatBox.log('');
653
- if (role === 'assistant') {
654
- chatBox.log(` {bold}{#d4a8ff-fg}Mint{/} {gray-fg}${timeStr}{/}`);
655
- }
656
-
657
- let buffer = ''; // accumulates the full response text
658
- let lineBuffer = ''; // current partial line being built
659
- let lineRendered = false; // whether we already pushed the first line prefix
660
-
661
- function flushLine(force = false) {
662
- // Flush content that fits on one line-width or when forced
663
- if (!lineBuffer && !force) return;
664
- if (!lineRendered) {
665
- chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
666
- lineRendered = true;
667
- } else {
668
- // Overwrite the last line by popping + re-pushing (blessed.log limitation)
669
- // We can't truly overwrite, so we just keep appending new lines for each chunk.
670
- // For large chunks, split on newline and emit per-line.
671
- chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
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
- screen.render();
674
- }
675
-
676
- function appendChunk(text) {
677
- if (!text) return;
678
- buffer += text;
679
- const segments = text.split('\n');
680
- for (let i = 0; i < segments.length; i++) {
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
- // Otherwise keep buffering the partial line
698
- }
699
- }
700
-
701
- function finalize(timestamp = null) {
702
- // Flush remaining buffer
703
- if (lineBuffer) {
704
- const lines = wrapLineSmart(lineBuffer, maxLineWidth);
705
- lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
706
- lineBuffer = '';
707
- }
708
- // Track last response for clipboard
709
- lastAssistantResponse = buffer;
710
- screen.render();
711
- }
712
-
713
- return { appendChunk, finalize };
714
- }
715
-
716
- function appendCodeStep(info) {
717
- if (typeof info === 'string') {
718
- appendMessage('system', `[Code] ${info}`);
719
- return;
720
- }
721
-
722
- const { step, phase, action, target, message, thought } = info;
723
- const maxLineWidth = Math.max(screen.width - 20, 36);
724
-
725
- // Special handling for ask_user which needs a box style
726
- if (action === 'ask_user') {
727
- chatBox.log('');
728
- chatBox.log(` {#88e0b0-fg}✓{/} {bold}Ask User{/}`);
729
- const questionLines = wrapText(target || message || '', maxLineWidth - 6);
730
- questionLines.forEach(l => chatBox.log(` {#95a2b8-fg}${l}{/}`));
731
- screen.render();
732
- return;
733
- }
734
-
735
- let icon = '{#88e0b0-fg}✓{/}';
736
- let label = action || phase;
737
- let color = '{#ffffff-fg}';
738
-
739
- // Map internal action names to display names seen in the screenshot
740
- switch (action) {
741
- case 'thinking':
742
- if (phase === 'thinking' && !thought) {
743
- // Initial "Thinking..." without a bubble
744
- chatBox.log('');
745
- chatBox.log(` {#ffd166-fg}* {bold}Thinking{/}`);
746
- } else if (thought) {
747
- // Show reasoning bubble
748
- const thoughtLines = wrapText(thought, maxLineWidth - 6);
749
- thoughtLines.forEach(l => chatBox.log(` {gray-fg}> ${l}{/}`));
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
- approvalDialog.ask(message, (approved) => {
819
- inputBox.focus();
820
- inputBox.readInput(); // Ensure we resume reading after dialog
821
- refreshInputStyles();
822
- screen.render();
823
- resolve(Boolean(approved));
824
- });
825
- });
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
- // We use a simple textbox floating over the chat or reuse the main input?
836
- // Reusing the main input is cleaner for CLI.
837
- // But we need to change the label to ' Answer '
838
- const oldLabel = inputBox._label.content;
839
- inputBox._label.setContent(' Answer ');
840
-
841
- // Clear input for the answer
842
- inputBox.clearValue();
843
- hidePlaceholder();
844
- inputBox.focus();
845
- inputBox.readInput();
846
- screen.render();
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
- inputBox.on('submit', submitHandler);
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
- return { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode, appendCodeStep, updateWorkspace, askUser };
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 };