@pheem49/mint 1.2.4 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -23
- package/index.html +16 -0
- package/main.js +36 -83
- package/mint-cli-logic.js +19 -0
- package/mint-cli.js +316 -39
- package/package.json +20 -3
- package/src/AI_Brain/Gemini_API.js +439 -20
- package/src/AI_Brain/agent_orchestrator.js +73 -0
- package/src/AI_Brain/autonomous_brain.js +2 -0
- package/src/AI_Brain/knowledge_base.js +199 -125
- package/src/AI_Brain/memory_store.js +318 -0
- package/src/Automation_Layer/file_operations.js +41 -19
- package/src/CLI/chat_router.js +181 -0
- package/src/CLI/chat_ui.js +321 -110
- package/src/CLI/code_agent.js +556 -0
- package/src/CLI/code_session_memory.js +62 -0
- package/src/CLI/list_features.js +1 -0
- package/src/CLI/onboarding.js +53 -6
- package/src/CLI/workspace_manager.js +81 -0
- package/src/Plugins/mcp_manager.js +95 -0
- package/src/Plugins/plugin_manager.js +2 -2
- package/src/Plugins/spotify.js +168 -40
- package/src/Plugins/system_monitor.js +72 -0
- package/src/System/config_manager.js +61 -8
- package/src/System/granular_automation.js +88 -0
- package/src/System/notifications.js +23 -0
- package/src/UI/settings.html +167 -65
- package/src/UI/settings.js +253 -42
- package/tests/agent_orchestrator.test.js +41 -0
- package/tests/config_manager.test.js +141 -0
- package/tests/memory_store.test.js +185 -0
- package/tests/spotify.test.js +201 -0
- package/tests/system_monitor.test.js +37 -0
- package/tests/workspace_manager.test.js +41 -0
package/src/CLI/chat_ui.js
CHANGED
|
@@ -6,14 +6,18 @@ const blessed = require('blessed');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
8
|
const { readConfig } = require('../System/config_manager');
|
|
9
|
-
const fs = require('fs');
|
|
10
9
|
|
|
11
10
|
const SLASH_COMMANDS = [
|
|
11
|
+
{ name: '/code', desc: 'Force workspace code mode for a task' },
|
|
12
12
|
{ name: '/models', desc: 'List or switch Gemini models' },
|
|
13
13
|
{ name: '/config', desc: 'Show current configuration' },
|
|
14
14
|
{ name: '/copy', desc: 'Copy last response to clipboard' },
|
|
15
15
|
{ name: '/clear', desc: 'Clear conversation history' },
|
|
16
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)' },
|
|
17
21
|
{ name: '/help', desc: 'Show help information' },
|
|
18
22
|
{ name: '/exit', desc: 'Exit Mint' }
|
|
19
23
|
];
|
|
@@ -29,6 +33,9 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
29
33
|
const config = readConfig();
|
|
30
34
|
const modelName = config.geminiModel || 'gemini';
|
|
31
35
|
const workspaceName = path.basename(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';
|
|
32
39
|
|
|
33
40
|
// ─── Screen ───────────────────────────────────────────────────────────────
|
|
34
41
|
const screen = blessed.screen({
|
|
@@ -40,82 +47,80 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
40
47
|
|
|
41
48
|
// ─── Banner ───────────────────────────────────────────────────────────────
|
|
42
49
|
const banner = blessed.box({
|
|
43
|
-
top: 0, left:
|
|
50
|
+
top: 0, left: 1, width: '100%-2', height: 4,
|
|
44
51
|
tags: true,
|
|
45
|
-
|
|
52
|
+
padding: { left: 1, right: 1 },
|
|
53
|
+
style: { bg: 'default', fg: '#d7dde8' }
|
|
46
54
|
});
|
|
47
55
|
banner.setContent([
|
|
48
|
-
`{
|
|
49
|
-
`{
|
|
50
|
-
`{
|
|
51
|
-
`{
|
|
52
|
-
`{bold}{#88e0b0-fg} | | | | | | | | |_ | |____| |____ _| |_ {/}`,
|
|
53
|
-
`{bold}{#88e0b0-fg} |_| |_|_|_| |_|\\__| \\_____|______|_____|{/}`,
|
|
54
|
-
``,
|
|
55
|
-
`{bold} Welcome to Mint Interactive AI!{/} {gray-fg}Type '/help' for commands · 'exit' or Esc to quit{/}`
|
|
56
|
+
`{#88e0b0-fg} __ __ _ _ ___ _ ___ {/}`,
|
|
57
|
+
`{#88e0b0-fg}| \\/ (_)_ __ | |_ / __| | |_ _|{/}`,
|
|
58
|
+
`{#88e0b0-fg}| |\\/| | | '_ \\| _| (__| |__ | | {/}`,
|
|
59
|
+
`{#88e0b0-fg}|_| |_|_|_| |_|\\__|\\___|____|___|{/}`
|
|
56
60
|
].join('\n'));
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
style: { fg: '#
|
|
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' }
|
|
63
67
|
});
|
|
64
68
|
|
|
65
69
|
// ─── Chat log (scrollable) ────────────────────────────────────────────────
|
|
66
70
|
const chatBox = blessed.log({
|
|
67
|
-
top:
|
|
68
|
-
bottom: 8,
|
|
71
|
+
top: 6, left: 1, width: '100%-2',
|
|
72
|
+
bottom: 8,
|
|
69
73
|
tags: true,
|
|
70
74
|
scrollable: true,
|
|
71
75
|
alwaysScroll: true,
|
|
72
|
-
scrollbar: { ch: '
|
|
73
|
-
style: { bg: '
|
|
76
|
+
scrollbar: { ch: '┃', style: { fg: '#335d52' } },
|
|
77
|
+
style: { bg: '#171b24', fg: '#ffffff', border: { fg: '#2f3747' } },
|
|
74
78
|
mouse: true,
|
|
75
|
-
scrollable: true
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const divider2 = blessed.line({
|
|
80
|
-
bottom: 7, left: 0, width: '100%',
|
|
81
|
-
orientation: 'horizontal',
|
|
82
|
-
style: { fg: '#333333' }
|
|
79
|
+
scrollable: true,
|
|
80
|
+
border: { type: 'line' },
|
|
81
|
+
padding: { left: 1, right: 1, top: 0, bottom: 0 },
|
|
82
|
+
label: ' Conversation '
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
// ─── Hint bar ─────────────────────────────────────────────────────────────
|
|
86
86
|
const hintBar = blessed.box({
|
|
87
|
-
bottom: 6, left:
|
|
87
|
+
bottom: 6, left: 1, width: '100%-2', height: 1,
|
|
88
88
|
tags: true,
|
|
89
|
-
content:
|
|
89
|
+
content: HINT_DEFAULT,
|
|
90
90
|
style: { bg: 'default' }
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
// ─── Input area ───────────────────────────────────────────────────────────
|
|
94
|
-
const inputBox = blessed.
|
|
95
|
-
bottom: 3, left:
|
|
94
|
+
const inputBox = blessed.textbox({
|
|
95
|
+
bottom: 3, left: 1, width: '100%-2', height: 3,
|
|
96
96
|
tags: false,
|
|
97
97
|
inputOnFocus: true,
|
|
98
98
|
keys: true,
|
|
99
99
|
style: {
|
|
100
|
-
bg:
|
|
101
|
-
fg:
|
|
102
|
-
border: { fg: '#
|
|
103
|
-
focus: {
|
|
100
|
+
bg: INPUT_BG,
|
|
101
|
+
fg: INPUT_FG,
|
|
102
|
+
border: { fg: '#335d52' },
|
|
103
|
+
focus: {
|
|
104
|
+
fg: INPUT_FG,
|
|
105
|
+
bg: INPUT_BG,
|
|
106
|
+
border: { fg: '#88e0b0' }
|
|
107
|
+
}
|
|
104
108
|
},
|
|
105
109
|
border: { type: 'line' },
|
|
106
|
-
padding: { left: 1 }
|
|
110
|
+
padding: { left: 1 },
|
|
111
|
+
label: ' Message '
|
|
107
112
|
});
|
|
108
113
|
|
|
109
114
|
// ─── Placeholder (SIBLING widget floating over input content area) ─────────
|
|
110
115
|
// inputBox: bottom=3, height=3, border=1 → content row at bottom=4, left=2
|
|
111
116
|
const placeholderWidget = blessed.text({
|
|
112
117
|
bottom: 4, // inside input content area (border offset)
|
|
113
|
-
left:
|
|
114
|
-
width: '100%-
|
|
118
|
+
left: 3,
|
|
119
|
+
width: '100%-6',
|
|
115
120
|
height: 1,
|
|
116
|
-
content: '>
|
|
121
|
+
content: '> Ask anything, or describe a coding task for this workspace',
|
|
117
122
|
tags: false,
|
|
118
|
-
style: { fg: '#
|
|
123
|
+
style: { fg: '#5d6678', bg: '#10141c' }
|
|
119
124
|
});
|
|
120
125
|
|
|
121
126
|
let placeholderVisible = true;
|
|
@@ -136,12 +141,40 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
function refreshInputStyles() {
|
|
145
|
+
inputBox.style.fg = INPUT_FG;
|
|
146
|
+
inputBox.style.bg = INPUT_BG;
|
|
147
|
+
if (inputBox.style.focus) {
|
|
148
|
+
inputBox.style.focus.fg = INPUT_FG;
|
|
149
|
+
inputBox.style.focus.bg = INPUT_BG;
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(inputBox.children)) {
|
|
152
|
+
inputBox.children.forEach((child) => {
|
|
153
|
+
if (child.style) {
|
|
154
|
+
child.style.fg = INPUT_FG;
|
|
155
|
+
child.style.bg = INPUT_BG;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
applyTerminalInputAttrs();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function applyTerminalInputAttrs() {
|
|
163
|
+
try {
|
|
164
|
+
if (!screen || !screen.program || typeof inputBox.sattr !== 'function' || typeof screen.codeAttr !== 'function') {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const attr = inputBox.sattr(inputBox.style);
|
|
168
|
+
screen.program.write(screen.codeAttr(attr));
|
|
169
|
+
} catch (_) {}
|
|
170
|
+
}
|
|
171
|
+
|
|
139
172
|
// ─── Status bar (3 columns: left / center / right) ──────────────────────
|
|
140
173
|
const statusBar = blessed.box({
|
|
141
|
-
bottom: 0, left:
|
|
174
|
+
bottom: 0, left: 1, width: '100%-2', height: 3,
|
|
142
175
|
tags: true,
|
|
143
|
-
style: { bg: '#
|
|
144
|
-
border: { type: 'line', fg: '#
|
|
176
|
+
style: { bg: '#10141c', fg: '#888888' },
|
|
177
|
+
border: { type: 'line', fg: '#222c38' }
|
|
145
178
|
});
|
|
146
179
|
|
|
147
180
|
// Left: workspace info
|
|
@@ -152,20 +185,20 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
152
185
|
height: 1,
|
|
153
186
|
tags: true,
|
|
154
187
|
content: ` workspace {bold}(${workspaceName}){/bold}`,
|
|
155
|
-
style: { bg: '#
|
|
188
|
+
style: { bg: '#10141c', fg: '#93a0b7' }
|
|
156
189
|
});
|
|
157
190
|
|
|
158
|
-
// Center:
|
|
191
|
+
// Center: mode + status
|
|
159
192
|
const statusCenter = blessed.text({
|
|
160
193
|
parent: statusBar,
|
|
161
194
|
top: 0,
|
|
162
195
|
left: 'center',
|
|
163
|
-
width: '
|
|
196
|
+
width: '44%',
|
|
164
197
|
height: 1,
|
|
165
198
|
align: 'center',
|
|
166
199
|
tags: true,
|
|
167
|
-
content: `{#cc4444-fg}no sandbox{/}`,
|
|
168
|
-
style: { bg: '#
|
|
200
|
+
content: `{#88aaff-fg}[Chat]{/} {#cc4444-fg}no sandbox{/}`,
|
|
201
|
+
style: { bg: '#10141c', fg: '#888888' }
|
|
169
202
|
});
|
|
170
203
|
|
|
171
204
|
// Right: current model
|
|
@@ -177,18 +210,30 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
177
210
|
align: 'right',
|
|
178
211
|
tags: true,
|
|
179
212
|
content: `{#88e0b0-fg}${modelName}{/}`,
|
|
180
|
-
style: { bg: '#
|
|
213
|
+
style: { bg: '#10141c', fg: '#88e0b0' }
|
|
181
214
|
});
|
|
182
215
|
|
|
216
|
+
let activeMode = 'Chat';
|
|
217
|
+
|
|
218
|
+
function formatModeTag(mode) {
|
|
219
|
+
if (mode === 'Code') return `{#ffd166-fg}[Code]{/}`;
|
|
220
|
+
return `{#88aaff-fg}[Chat]{/}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
183
223
|
function updateStatusBar(thinkingText = null) {
|
|
184
224
|
if (thinkingText) {
|
|
185
|
-
statusCenter.setContent(
|
|
225
|
+
statusCenter.setContent(`${formatModeTag(activeMode)} {#88e0b0-fg}${thinkingText}{/}`);
|
|
186
226
|
} else {
|
|
187
|
-
statusCenter.setContent(
|
|
227
|
+
statusCenter.setContent(`${formatModeTag(activeMode)} {#cc4444-fg}no sandbox{/}`);
|
|
188
228
|
}
|
|
189
229
|
screen.render();
|
|
190
230
|
}
|
|
191
231
|
|
|
232
|
+
function setMode(mode) {
|
|
233
|
+
activeMode = mode === 'Code' ? 'Code' : 'Chat';
|
|
234
|
+
updateStatusBar(null);
|
|
235
|
+
}
|
|
236
|
+
|
|
192
237
|
/** Update model name in status bar (called after /models switch) */
|
|
193
238
|
function updateStatusModel(newModel) {
|
|
194
239
|
statusRight.setContent(`{#88e0b0-fg}${newModel}{/}`);
|
|
@@ -198,9 +243,8 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
198
243
|
|
|
199
244
|
// ─── Append widgets to screen ─────────────────────────────────────────────
|
|
200
245
|
screen.append(banner);
|
|
201
|
-
screen.append(
|
|
246
|
+
screen.append(subBanner);
|
|
202
247
|
screen.append(chatBox);
|
|
203
|
-
screen.append(divider2);
|
|
204
248
|
screen.append(hintBar);
|
|
205
249
|
screen.append(inputBox);
|
|
206
250
|
screen.append(statusBar);
|
|
@@ -209,9 +253,9 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
209
253
|
// ─── Suggestion List ──────────────────────────────────────────────────────
|
|
210
254
|
const commandList = blessed.list({
|
|
211
255
|
parent: screen,
|
|
212
|
-
bottom: 6,
|
|
213
|
-
left:
|
|
214
|
-
width: '
|
|
256
|
+
bottom: 6,
|
|
257
|
+
left: 3,
|
|
258
|
+
width: '64%',
|
|
215
259
|
height: 8,
|
|
216
260
|
tags: true,
|
|
217
261
|
keys: false, // We will handle keys manually to keep focus on input
|
|
@@ -219,10 +263,10 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
219
263
|
hidden: true,
|
|
220
264
|
border: { type: 'line', fg: '#88e0b0' },
|
|
221
265
|
style: {
|
|
222
|
-
bg: '#
|
|
266
|
+
bg: '#10141c',
|
|
223
267
|
fg: '#ffffff',
|
|
224
268
|
selected: {
|
|
225
|
-
bg: '#
|
|
269
|
+
bg: '#22352f',
|
|
226
270
|
fg: '#88e0b0',
|
|
227
271
|
bold: true
|
|
228
272
|
}
|
|
@@ -230,6 +274,22 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
230
274
|
});
|
|
231
275
|
|
|
232
276
|
let activeSuggestions = [];
|
|
277
|
+
const approvalDialog = blessed.question({
|
|
278
|
+
parent: screen,
|
|
279
|
+
tags: true,
|
|
280
|
+
border: { type: 'line', fg: '#88e0b0' },
|
|
281
|
+
style: {
|
|
282
|
+
bg: '#10141c',
|
|
283
|
+
fg: '#ffffff',
|
|
284
|
+
border: { fg: '#88e0b0' }
|
|
285
|
+
},
|
|
286
|
+
width: '80%',
|
|
287
|
+
height: 'shrink',
|
|
288
|
+
top: 'center',
|
|
289
|
+
left: 'center',
|
|
290
|
+
label: ' Approval ',
|
|
291
|
+
hidden: true
|
|
292
|
+
});
|
|
233
293
|
|
|
234
294
|
function updateSuggestions(filter = '') {
|
|
235
295
|
activeSuggestions = SLASH_COMMANDS.filter(cmd =>
|
|
@@ -260,6 +320,7 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
260
320
|
|
|
261
321
|
// Consolidated key handling
|
|
262
322
|
inputBox.on('element keypress', (el, ch, key) => {
|
|
323
|
+
refreshInputStyles();
|
|
263
324
|
// 1. Handle placeholder visibility
|
|
264
325
|
if (!key.ctrl && !key.meta && key.name !== 'enter' && key.name !== 'tab') {
|
|
265
326
|
if (ch) hidePlaceholder();
|
|
@@ -287,6 +348,7 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
287
348
|
|
|
288
349
|
// 3. Logic for suggestions and placeholder after key is processed
|
|
289
350
|
setImmediate(() => {
|
|
351
|
+
refreshInputStyles();
|
|
290
352
|
const val = (inputBox.getValue ? inputBox.getValue() : inputBox.value) || '';
|
|
291
353
|
const isCommand = val.startsWith('/') && !val.includes(' ');
|
|
292
354
|
|
|
@@ -308,6 +370,15 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
308
370
|
});
|
|
309
371
|
});
|
|
310
372
|
|
|
373
|
+
inputBox.on('focus', () => {
|
|
374
|
+
refreshInputStyles();
|
|
375
|
+
screen.render();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
inputBox.on('keypress', () => {
|
|
379
|
+
applyTerminalInputAttrs();
|
|
380
|
+
});
|
|
381
|
+
|
|
311
382
|
|
|
312
383
|
// Submit or Select Suggestion on Enter
|
|
313
384
|
inputBox.key(['enter'], () => {
|
|
@@ -318,6 +389,7 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
318
389
|
commandList.hide();
|
|
319
390
|
hidePlaceholder();
|
|
320
391
|
inputBox.focus();
|
|
392
|
+
refreshInputStyles();
|
|
321
393
|
screen.render();
|
|
322
394
|
return; // Don't submit yet, let user add args or press enter again
|
|
323
395
|
}
|
|
@@ -325,12 +397,20 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
325
397
|
|
|
326
398
|
const raw = (inputBox.getValue ? inputBox.getValue() : inputBox.value) || '';
|
|
327
399
|
const text = raw.trim();
|
|
328
|
-
if (!text)
|
|
400
|
+
if (!text) {
|
|
401
|
+
inputBox.clearValue();
|
|
402
|
+
showPlaceholder();
|
|
403
|
+
inputBox.focus();
|
|
404
|
+
refreshInputStyles();
|
|
405
|
+
screen.render();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
329
408
|
|
|
330
409
|
// Clear input and restore placeholder
|
|
331
410
|
inputBox.clearValue();
|
|
332
411
|
showPlaceholder();
|
|
333
412
|
inputBox.focus();
|
|
413
|
+
refreshInputStyles();
|
|
334
414
|
screen.render();
|
|
335
415
|
|
|
336
416
|
if (text.toLowerCase() === 'exit' || text.toLowerCase() === 'quit') {
|
|
@@ -342,18 +422,9 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
342
422
|
});
|
|
343
423
|
|
|
344
424
|
// Shift+Enter = newline in input
|
|
345
|
-
inputBox.key(['S-enter'], () => {
|
|
346
|
-
hidePlaceholder();
|
|
347
|
-
const val = (inputBox.getValue ? inputBox.getValue() : inputBox.value) || '';
|
|
348
|
-
inputBox.setValue(val + '\n');
|
|
349
|
-
screen.render();
|
|
350
|
-
});
|
|
351
|
-
|
|
352
425
|
// Ctrl+C — double-press to exit
|
|
353
426
|
let ctrlCPressed = false;
|
|
354
427
|
let ctrlCTimer = null;
|
|
355
|
-
const HINT_DEFAULT = `{gray-fg} Shift+Drag to select text · Ctrl+Y to copy · /help for commands{/}`;
|
|
356
|
-
|
|
357
428
|
screen.key(['C-c'], () => {
|
|
358
429
|
if (ctrlCPressed) {
|
|
359
430
|
clearTimeout(ctrlCTimer);
|
|
@@ -415,6 +486,7 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
415
486
|
|
|
416
487
|
// ─── Initial render ───────────────────────────────────────────────────────
|
|
417
488
|
inputBox.focus();
|
|
489
|
+
refreshInputStyles();
|
|
418
490
|
screen.render();
|
|
419
491
|
|
|
420
492
|
// ─── Public API ───────────────────────────────────────────────────────────
|
|
@@ -427,63 +499,177 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
427
499
|
* @param {string} text
|
|
428
500
|
* @param {string} timestamp - ISO string or Date object
|
|
429
501
|
*/
|
|
430
|
-
function
|
|
431
|
-
|
|
432
|
-
|
|
502
|
+
function wrapLineSmart(line, width) {
|
|
503
|
+
if (line.length <= width) return [line];
|
|
504
|
+
if (!line.includes(' ')) {
|
|
505
|
+
const pieces = [];
|
|
506
|
+
for (let index = 0; index < line.length; index += width) {
|
|
507
|
+
pieces.push(line.slice(index, index + width));
|
|
508
|
+
}
|
|
509
|
+
return pieces;
|
|
510
|
+
}
|
|
433
511
|
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
continue;
|
|
512
|
+
const words = line.split(/\s+/);
|
|
513
|
+
const lines = [];
|
|
514
|
+
let current = '';
|
|
515
|
+
for (const word of words) {
|
|
516
|
+
if (word.length > width) {
|
|
517
|
+
if (current) {
|
|
518
|
+
lines.push(current);
|
|
519
|
+
current = '';
|
|
443
520
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (current.length >= width) {
|
|
451
|
-
lines.push(current);
|
|
452
|
-
current = '';
|
|
521
|
+
for (let index = 0; index < word.length; index += width) {
|
|
522
|
+
const slice = word.slice(index, index + width);
|
|
523
|
+
if (slice.length === width) {
|
|
524
|
+
lines.push(slice);
|
|
525
|
+
} else {
|
|
526
|
+
current = slice;
|
|
453
527
|
}
|
|
454
528
|
}
|
|
455
|
-
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (!current) {
|
|
533
|
+
current = word;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (`${current} ${word}`.length <= width) {
|
|
538
|
+
current += ` ${word}`;
|
|
539
|
+
} else {
|
|
540
|
+
lines.push(current);
|
|
541
|
+
current = word;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (current) lines.push(current);
|
|
545
|
+
return lines;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function wrapText(str, width) {
|
|
549
|
+
const lines = [];
|
|
550
|
+
const originalLines = String(str).split('\n');
|
|
551
|
+
for (const line of originalLines) {
|
|
552
|
+
if (line.length === 0) {
|
|
553
|
+
lines.push('');
|
|
554
|
+
continue;
|
|
456
555
|
}
|
|
457
|
-
|
|
458
|
-
}
|
|
556
|
+
lines.push(...wrapLineSmart(line, width));
|
|
557
|
+
}
|
|
558
|
+
return lines;
|
|
559
|
+
}
|
|
459
560
|
|
|
460
|
-
|
|
561
|
+
function appendMessage(role, text, timestamp = null) {
|
|
562
|
+
const now = timestamp ? new Date(timestamp) : new Date();
|
|
563
|
+
const timeStr = now.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
564
|
+
const maxLineWidth = Math.max(screen.width - 20, 36);
|
|
565
|
+
const lines = wrapText(text, maxLineWidth);
|
|
461
566
|
|
|
462
567
|
if (role === 'user') {
|
|
463
|
-
chatBox.log(
|
|
464
|
-
|
|
465
|
-
lines.forEach(l => chatBox.log(`
|
|
466
|
-
chatBox.log(` {gray-fg}${timeStr}{/}`);
|
|
568
|
+
chatBox.log(``);
|
|
569
|
+
chatBox.log(` {bold}{#88e0b0-fg}You{/} {gray-fg}${timeStr}{/}`);
|
|
570
|
+
lines.forEach(l => chatBox.log(` {#88e0b0-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
467
571
|
} else if (role === 'assistant') {
|
|
468
572
|
lastAssistantResponse = text;
|
|
469
|
-
chatBox.log(
|
|
470
|
-
|
|
471
|
-
lines.forEach(l => chatBox.log(`
|
|
472
|
-
chatBox.log(` {#444444-fg}┕${'─'.repeat(4)}{/} {gray-fg}${timeStr}{/}`);
|
|
573
|
+
chatBox.log(``);
|
|
574
|
+
chatBox.log(` {bold}{#d4a8ff-fg}Mint{/} {gray-fg}${timeStr}{/}`);
|
|
575
|
+
lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
473
576
|
} else if (role === 'system') {
|
|
474
|
-
const displayTag = text.startsWith('Action:')
|
|
577
|
+
const displayTag = text.startsWith('Action:')
|
|
578
|
+
? '{#88e0b0-fg}Action{/}'
|
|
579
|
+
: text.startsWith('[Code]')
|
|
580
|
+
? '{#ffd166-fg}Code{/}'
|
|
581
|
+
: '{#8ba0ff-fg}System{/}';
|
|
475
582
|
const cleanText = text.replace(/^(Action:|System:)\s*/, '');
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
583
|
+
const systemLines = wrapText(cleanText, maxLineWidth - 4);
|
|
584
|
+
chatBox.log(``);
|
|
585
|
+
chatBox.log(` {bold}${displayTag}{/}`);
|
|
586
|
+
systemLines.forEach(l => chatBox.log(` {#95a2b8-fg}${l}{/}`));
|
|
479
587
|
} else if (role === 'error') {
|
|
480
|
-
chatBox.log(
|
|
481
|
-
|
|
482
|
-
lines.forEach(l => chatBox.log(`
|
|
588
|
+
chatBox.log(``);
|
|
589
|
+
chatBox.log(` {bold}{#ff6b6b-fg}Error{/} {gray-fg}${timeStr}{/}`);
|
|
590
|
+
lines.forEach(l => chatBox.log(` {#7a2e2e-fg}▏{/} {#ff7d7d-fg}${l}{/}`));
|
|
483
591
|
}
|
|
484
592
|
screen.render();
|
|
485
593
|
}
|
|
486
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Opens a streaming message bubble for the assistant.
|
|
597
|
+
* Returns { appendChunk(text), finalize(timestamp) } for typewriter rendering.
|
|
598
|
+
* Usage:
|
|
599
|
+
* const stream = streamMessage('assistant');
|
|
600
|
+
* stream.appendChunk('Hello'); stream.appendChunk(' World');
|
|
601
|
+
* stream.finalize(timestamp);
|
|
602
|
+
*/
|
|
603
|
+
function streamMessage(role = 'assistant') {
|
|
604
|
+
const now = new Date();
|
|
605
|
+
const timeStr = now.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
606
|
+
const maxLineWidth = Math.max(screen.width - 20, 36);
|
|
607
|
+
|
|
608
|
+
// Print the header bubble once
|
|
609
|
+
chatBox.log('');
|
|
610
|
+
if (role === 'assistant') {
|
|
611
|
+
chatBox.log(` {bold}{#d4a8ff-fg}Mint{/} {gray-fg}${timeStr}{/}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let buffer = ''; // accumulates the full response text
|
|
615
|
+
let lineBuffer = ''; // current partial line being built
|
|
616
|
+
let lineRendered = false; // whether we already pushed the first line prefix
|
|
617
|
+
|
|
618
|
+
function flushLine(force = false) {
|
|
619
|
+
// Flush content that fits on one line-width or when forced
|
|
620
|
+
if (!lineBuffer && !force) return;
|
|
621
|
+
if (!lineRendered) {
|
|
622
|
+
chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
|
|
623
|
+
lineRendered = true;
|
|
624
|
+
} else {
|
|
625
|
+
// Overwrite the last line by popping + re-pushing (blessed.log limitation)
|
|
626
|
+
// We can't truly overwrite, so we just keep appending new lines for each chunk.
|
|
627
|
+
// For large chunks, split on newline and emit per-line.
|
|
628
|
+
chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
|
|
629
|
+
}
|
|
630
|
+
screen.render();
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function appendChunk(text) {
|
|
634
|
+
if (!text) return;
|
|
635
|
+
buffer += text;
|
|
636
|
+
const segments = text.split('\n');
|
|
637
|
+
for (let i = 0; i < segments.length; i++) {
|
|
638
|
+
lineBuffer += segments[i];
|
|
639
|
+
if (i < segments.length - 1) {
|
|
640
|
+
// Newline boundary — emit current line
|
|
641
|
+
const lines = wrapLineSmart(lineBuffer, maxLineWidth);
|
|
642
|
+
lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
643
|
+
lineBuffer = '';
|
|
644
|
+
lineRendered = true;
|
|
645
|
+
screen.render();
|
|
646
|
+
} else if (lineBuffer.length >= maxLineWidth) {
|
|
647
|
+
// Line overflow — auto-wrap
|
|
648
|
+
const lines = wrapLineSmart(lineBuffer, maxLineWidth);
|
|
649
|
+
lines.slice(0, -1).forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
650
|
+
lineBuffer = lines[lines.length - 1] || '';
|
|
651
|
+
lineRendered = true;
|
|
652
|
+
screen.render();
|
|
653
|
+
}
|
|
654
|
+
// Otherwise keep buffering the partial line
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function finalize(timestamp = null) {
|
|
659
|
+
// Flush remaining buffer
|
|
660
|
+
if (lineBuffer) {
|
|
661
|
+
const lines = wrapLineSmart(lineBuffer, maxLineWidth);
|
|
662
|
+
lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
663
|
+
lineBuffer = '';
|
|
664
|
+
}
|
|
665
|
+
// Track last response for clipboard
|
|
666
|
+
lastAssistantResponse = buffer;
|
|
667
|
+
screen.render();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return { appendChunk, finalize };
|
|
671
|
+
}
|
|
672
|
+
|
|
487
673
|
/** Show/hide thinking indicator in status bar */
|
|
488
674
|
function setThinking(active, secondsElapsed = 0) {
|
|
489
675
|
if (active) {
|
|
@@ -499,7 +685,32 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
499
685
|
return copyToClipboard(lastAssistantResponse);
|
|
500
686
|
}
|
|
501
687
|
|
|
502
|
-
|
|
688
|
+
function requestApproval(request) {
|
|
689
|
+
return new Promise((resolve) => {
|
|
690
|
+
const typeLabel = request.type === 'shell'
|
|
691
|
+
? 'Shell Command'
|
|
692
|
+
: request.type === 'patch'
|
|
693
|
+
? 'Patch Edit'
|
|
694
|
+
: 'File Write';
|
|
695
|
+
const preview = request.preview || request.label || '';
|
|
696
|
+
const message = [
|
|
697
|
+
`{bold}${typeLabel}{/bold}`,
|
|
698
|
+
'',
|
|
699
|
+
preview,
|
|
700
|
+
'',
|
|
701
|
+
'Approve this action?'
|
|
702
|
+
].join('\n');
|
|
703
|
+
|
|
704
|
+
approvalDialog.ask(message, (approved) => {
|
|
705
|
+
inputBox.focus();
|
|
706
|
+
refreshInputStyles();
|
|
707
|
+
screen.render();
|
|
708
|
+
resolve(Boolean(approved));
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode };
|
|
503
714
|
}
|
|
504
715
|
|
|
505
716
|
module.exports = { createChatUI };
|