@pheem49/mint 1.2.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/BUILD_AND_RELEASE.md +75 -0
- package/LICENSE +654 -0
- package/README.md +165 -0
- package/assets/Agent_Mint.png +0 -0
- package/assets/CLI_Screen.png +0 -0
- package/assets/Settings.png +0 -0
- package/assets/icon.png +0 -0
- package/benchmark_ai.js +71 -0
- package/main.js +968 -0
- package/mint-cli-logic.js +71 -0
- package/mint-cli.js +239 -0
- package/package.json +60 -0
- package/preload-picker.js +11 -0
- package/preload-settings.js +11 -0
- package/preload.js +37 -0
- package/privacy.txt +1 -0
- package/src/AI_Brain/Gemini_API.js +419 -0
- package/src/AI_Brain/autonomous_brain.js +139 -0
- package/src/AI_Brain/behavior_memory.js +114 -0
- package/src/AI_Brain/headless_agent.js +120 -0
- package/src/AI_Brain/knowledge_base.js +222 -0
- package/src/AI_Brain/proactive_engine.js +168 -0
- package/src/Automation_Layer/browser_automation.js +147 -0
- package/src/Automation_Layer/file_operations.js +80 -0
- package/src/Automation_Layer/open_app.js +56 -0
- package/src/Automation_Layer/open_website.js +38 -0
- package/src/CLI/chat_ui.js +468 -0
- package/src/CLI/list_features.js +56 -0
- package/src/CLI/onboarding.js +60 -0
- package/src/Command_Parser/parser.js +34 -0
- package/src/Plugins/dev_tools.js +41 -0
- package/src/Plugins/discord.js +20 -0
- package/src/Plugins/docker.js +45 -0
- package/src/Plugins/google_calendar.js +26 -0
- package/src/Plugins/obsidian.js +54 -0
- package/src/Plugins/plugin_manager.js +81 -0
- package/src/Plugins/spotify.js +45 -0
- package/src/Plugins/system_metrics.js +31 -0
- package/src/System/chat_history_manager.js +57 -0
- package/src/System/config_manager.js +73 -0
- package/src/System/custom_workflows.js +127 -0
- package/src/System/daemon_manager.js +67 -0
- package/src/System/system_automation.js +88 -0
- package/src/System/system_events.js +79 -0
- package/src/System/system_info.js +55 -0
- package/src/System/task_manager.js +85 -0
- package/src/UI/floating.css +80 -0
- package/src/UI/floating.html +17 -0
- package/src/UI/floating.js +67 -0
- package/src/UI/index.html +126 -0
- package/src/UI/preload-floating.js +7 -0
- package/src/UI/preload-spotlight.js +10 -0
- package/src/UI/preload-widget.js +5 -0
- package/src/UI/proactive-glow.html +42 -0
- package/src/UI/renderer.js +978 -0
- package/src/UI/screenPicker.html +214 -0
- package/src/UI/screenPicker.js +262 -0
- package/src/UI/settings.css +705 -0
- package/src/UI/settings.html +396 -0
- package/src/UI/settings.js +514 -0
- package/src/UI/spotlight.css +119 -0
- package/src/UI/spotlight.html +23 -0
- package/src/UI/spotlight.js +181 -0
- package/src/UI/styles.css +627 -0
- package/src/UI/widget.css +218 -0
- package/src/UI/widget.html +29 -0
- package/src/UI/widget.js +10 -0
- package/tech_news.txt +3 -0
- package/test_knowledge.txt +3 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
let shell;
|
|
3
|
+
try {
|
|
4
|
+
shell = require('electron').shell;
|
|
5
|
+
} catch (e) {
|
|
6
|
+
shell = null;
|
|
7
|
+
}
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* สร้างโฟลเดอร์ใหม่
|
|
14
|
+
* target: ชื่อโฟลเดอร์ หรือ absolute path
|
|
15
|
+
* ถ้าเป็นชื่อเดียว จะสร้างบน Desktop
|
|
16
|
+
*/
|
|
17
|
+
function createFolder(target) {
|
|
18
|
+
if (!target) return { success: false, message: 'No folder name provided.' };
|
|
19
|
+
|
|
20
|
+
let folderPath = target;
|
|
21
|
+
|
|
22
|
+
// ถ้าไม่ใช่ absolute path ให้สร้างบน Desktop
|
|
23
|
+
if (!path.isAbsolute(target)) {
|
|
24
|
+
const desktopPath = path.join(os.homedir(), 'Desktop');
|
|
25
|
+
folderPath = path.join(desktopPath, target);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
fs.mkdirSync(folderPath, { recursive: true });
|
|
30
|
+
console.log(`Folder created: ${folderPath}`);
|
|
31
|
+
return { success: true, path: folderPath };
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error('createFolder error:', err);
|
|
34
|
+
return { success: false, message: err.message };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* เปิดไฟล์หรือโฟลเดอร์ด้วย default app ของระบบ
|
|
40
|
+
*/
|
|
41
|
+
async function openFile(target) {
|
|
42
|
+
if (!target) return;
|
|
43
|
+
if (shell) {
|
|
44
|
+
const result = await shell.openPath(target);
|
|
45
|
+
if (result) console.error('openFile error:', result);
|
|
46
|
+
} else {
|
|
47
|
+
exec(`xdg-open "${target}"`, (err) => {
|
|
48
|
+
if (err) console.error("Failed to open path via xdg-open:", err);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* ลบไฟล์หรือโฟลเดอร์ (ย้ายไป Trash)
|
|
55
|
+
*/
|
|
56
|
+
async function deleteFile(target) {
|
|
57
|
+
if (!target) return { success: false, message: 'No path provided.' };
|
|
58
|
+
if (shell) {
|
|
59
|
+
try {
|
|
60
|
+
await shell.trashItem(target);
|
|
61
|
+
return { success: true };
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('deleteFile error:', err);
|
|
64
|
+
return { success: false, message: err.message };
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
exec(`gio trash "${target}"`, (err) => {
|
|
69
|
+
if (err) {
|
|
70
|
+
console.error("Failed to trash item via gio trash:", err);
|
|
71
|
+
resolve({ success: false, message: err.message });
|
|
72
|
+
} else {
|
|
73
|
+
resolve({ success: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { createFolder, openFile, deleteFile };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
|
|
3
|
+
function openApp(target) {
|
|
4
|
+
if (!target) return;
|
|
5
|
+
|
|
6
|
+
let cmd = '';
|
|
7
|
+
if (process.platform === 'win32') {
|
|
8
|
+
cmd = `start "" "${target}"`;
|
|
9
|
+
} else if (process.platform === 'darwin') {
|
|
10
|
+
if (!target.includes('/')) {
|
|
11
|
+
cmd = `open -X -a "${target}" || open -a "${target}"`;
|
|
12
|
+
} else {
|
|
13
|
+
cmd = `open "${target}"`;
|
|
14
|
+
}
|
|
15
|
+
} else {
|
|
16
|
+
const tLower = target.toLowerCase();
|
|
17
|
+
const tCapitalized = target.charAt(0).toUpperCase() + target.slice(1).toLowerCase();
|
|
18
|
+
|
|
19
|
+
// Try common linux patterns: gtk-launch, exact name, lowercase, flatpak
|
|
20
|
+
if (!target.includes('/')) {
|
|
21
|
+
const patterns = [
|
|
22
|
+
`gtk-launch ${target}`,
|
|
23
|
+
`gtk-launch ${tLower}`,
|
|
24
|
+
`gtk-launch ${tCapitalized}`,
|
|
25
|
+
`gtk-launch com.${tLower}app.${tCapitalized}`,
|
|
26
|
+
`gtk-launch com.${tLower}.${tCapitalized}`,
|
|
27
|
+
target,
|
|
28
|
+
tLower,
|
|
29
|
+
`flatpak run ${target}`, // In case target is already ID
|
|
30
|
+
`flatpak run com.${tLower}app.${tCapitalized}`,
|
|
31
|
+
`flatpak run com.${tLower}.${tCapitalized}`,
|
|
32
|
+
`flatpak run com.${tLower}.Browser`,
|
|
33
|
+
`flatpak run com.${tLower}.${target}`,
|
|
34
|
+
`flatpak run com.valvesoftware.Steam`,
|
|
35
|
+
`flatpak run net.lutris.Lutris`,
|
|
36
|
+
`snap run ${tLower}`
|
|
37
|
+
];
|
|
38
|
+
cmd = patterns.join(' || ');
|
|
39
|
+
} else {
|
|
40
|
+
cmd = `xdg-open "${target}"`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
exec(cmd, (error) => {
|
|
45
|
+
if (error) {
|
|
46
|
+
console.error(`exec error: ${error}`);
|
|
47
|
+
if (process.platform !== 'win32') {
|
|
48
|
+
exec(target.toLowerCase(), (err2) => {
|
|
49
|
+
if (err2) console.error("Fallback lowercase exec failed:", err2);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { openApp };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
|
|
3
|
+
let shell;
|
|
4
|
+
try {
|
|
5
|
+
shell = require('electron').shell;
|
|
6
|
+
} catch (e) {
|
|
7
|
+
shell = null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function openWebsite(targetUrl) {
|
|
11
|
+
if (!targetUrl) return;
|
|
12
|
+
let url = targetUrl;
|
|
13
|
+
if (!url.startsWith('http')) {
|
|
14
|
+
url = 'https://' + url;
|
|
15
|
+
}
|
|
16
|
+
if (shell) {
|
|
17
|
+
shell.openExternal(url);
|
|
18
|
+
} else {
|
|
19
|
+
// Fallback for Node.js (Linux focus)
|
|
20
|
+
exec(`xdg-open "${url}"`, (err) => {
|
|
21
|
+
if (err) console.error("Failed to open URL via xdg_open:", err);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function openSearch(query) {
|
|
27
|
+
if (!query) return;
|
|
28
|
+
const url = `https://www.google.com/search?q=${encodeURIComponent(query)}`;
|
|
29
|
+
if (shell) {
|
|
30
|
+
shell.openExternal(url);
|
|
31
|
+
} else {
|
|
32
|
+
exec(`xdg-open "${url}"`, (err) => {
|
|
33
|
+
if (err) console.error("Failed to open search via xdg-open:", err);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { openWebsite, openSearch };
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint CLI - Gemini-style TUI using blessed
|
|
3
|
+
* Provides a rich terminal UI with chat history, input box, and status bar
|
|
4
|
+
*/
|
|
5
|
+
const blessed = require('blessed');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const { readConfig } = require('../System/config_manager');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
|
|
11
|
+
const SLASH_COMMANDS = [
|
|
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: '/help', desc: 'Show help information' },
|
|
18
|
+
{ name: '/exit', desc: 'Exit Mint' }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates and returns the Mint chat TUI screen
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {Function} options.onSubmit - Called with (userInput: string) when user sends a message
|
|
25
|
+
* @param {Function} options.onExit - Called when user exits
|
|
26
|
+
* @returns {{ screen, appendMessage, setThinking }}
|
|
27
|
+
*/
|
|
28
|
+
function createChatUI({ onSubmit, onExit }) {
|
|
29
|
+
const config = readConfig();
|
|
30
|
+
const modelName = config.geminiModel || 'gemini';
|
|
31
|
+
const workspaceName = path.basename(process.cwd());
|
|
32
|
+
|
|
33
|
+
// ─── Screen ───────────────────────────────────────────────────────────────
|
|
34
|
+
const screen = blessed.screen({
|
|
35
|
+
smartCSR: true,
|
|
36
|
+
fullUnicode: true,
|
|
37
|
+
title: 'Mint CLI',
|
|
38
|
+
cursor: {
|
|
39
|
+
artificial: true,
|
|
40
|
+
shape: 'line',
|
|
41
|
+
blink: true,
|
|
42
|
+
color: '#88e0b0'
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ─── Banner ───────────────────────────────────────────────────────────────
|
|
47
|
+
const banner = blessed.box({
|
|
48
|
+
top: 0, left: 0, width: '100%', height: 9,
|
|
49
|
+
tags: true,
|
|
50
|
+
style: { bg: 'default' }
|
|
51
|
+
});
|
|
52
|
+
banner.setContent([
|
|
53
|
+
`{bold}{#88e0b0-fg} __ __ _ _ _____ _ _____ {/}`,
|
|
54
|
+
`{bold}{#88e0b0-fg} | \\/ (_) | | / ____| | |_ _|{/}`,
|
|
55
|
+
`{bold}{#88e0b0-fg} | \\ / |_ _ __ | |_ | | | | | | {/}`,
|
|
56
|
+
`{bold}{#88e0b0-fg} | |\\/| | | '_ \\| __| | | | | | | {/}`,
|
|
57
|
+
`{bold}{#88e0b0-fg} | | | | | | | | |_ | |____| |____ _| |_ {/}`,
|
|
58
|
+
`{bold}{#88e0b0-fg} |_| |_|_|_| |_|\\__| \\_____|______|_____|{/}`,
|
|
59
|
+
``,
|
|
60
|
+
`{bold} Welcome to Mint Interactive AI!{/} {gray-fg}Type '/help' for commands · 'exit' or Esc to quit{/}`
|
|
61
|
+
].join('\n'));
|
|
62
|
+
|
|
63
|
+
// ─── Divider under banner ─────────────────────────────────────────────────
|
|
64
|
+
const divider1 = blessed.line({
|
|
65
|
+
top: 9, left: 0, width: '100%',
|
|
66
|
+
orientation: 'horizontal',
|
|
67
|
+
style: { fg: '#333333' }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ─── Chat log (scrollable) ────────────────────────────────────────────────
|
|
71
|
+
const chatBox = blessed.log({
|
|
72
|
+
top: 10, left: 0, width: '100%',
|
|
73
|
+
bottom: 8, // statusbar(3) + hint(1) + inputBox(3) + divider(1)
|
|
74
|
+
tags: true,
|
|
75
|
+
scrollable: true,
|
|
76
|
+
alwaysScroll: true,
|
|
77
|
+
scrollbar: { ch: '│', style: { fg: '#334433' } },
|
|
78
|
+
style: { bg: 'default', fg: '#ffffff' }
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ─── Divider above input ──────────────────────────────────────────────────
|
|
82
|
+
const divider2 = blessed.line({
|
|
83
|
+
bottom: 7, left: 0, width: '100%',
|
|
84
|
+
orientation: 'horizontal',
|
|
85
|
+
style: { fg: '#333333' }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ─── Hint bar ─────────────────────────────────────────────────────────────
|
|
89
|
+
const hintBar = blessed.box({
|
|
90
|
+
bottom: 6, left: 0, width: '100%', height: 1,
|
|
91
|
+
tags: true,
|
|
92
|
+
content: `{gray-fg} Shift+Tab to accept edits · /help for slash commands{/}`,
|
|
93
|
+
style: { bg: 'default' }
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── Input area ───────────────────────────────────────────────────────────
|
|
97
|
+
const inputBox = blessed.textarea({
|
|
98
|
+
bottom: 3, left: 0, width: '100%', height: 3,
|
|
99
|
+
tags: false,
|
|
100
|
+
inputOnFocus: true,
|
|
101
|
+
keys: true,
|
|
102
|
+
style: {
|
|
103
|
+
bg: '#111111',
|
|
104
|
+
fg: '#ffffff',
|
|
105
|
+
border: { fg: '#334433' },
|
|
106
|
+
focus: { border: { fg: '#88e0b0' } }
|
|
107
|
+
},
|
|
108
|
+
border: { type: 'line' },
|
|
109
|
+
padding: { left: 1 }
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ─── Placeholder (SIBLING widget floating over input content area) ─────────
|
|
113
|
+
// inputBox: bottom=3, height=3, border=1 → content row at bottom=4, left=2
|
|
114
|
+
const placeholderWidget = blessed.text({
|
|
115
|
+
bottom: 4, // inside input content area (border offset)
|
|
116
|
+
left: 2, // border(1) + padding(1)
|
|
117
|
+
width: '100%-4', // minus borders and padding
|
|
118
|
+
height: 1,
|
|
119
|
+
content: '> Type your message or @path/to/file',
|
|
120
|
+
tags: false,
|
|
121
|
+
style: { fg: '#555555', bg: '#111111' }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let placeholderVisible = true;
|
|
125
|
+
|
|
126
|
+
function hidePlaceholder() {
|
|
127
|
+
if (placeholderVisible) {
|
|
128
|
+
placeholderVisible = false;
|
|
129
|
+
placeholderWidget.hide();
|
|
130
|
+
screen.render();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function showPlaceholder() {
|
|
135
|
+
if (!placeholderVisible) {
|
|
136
|
+
placeholderVisible = true;
|
|
137
|
+
placeholderWidget.show();
|
|
138
|
+
screen.render();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Status bar (3 columns: left / center / right) ──────────────────────
|
|
143
|
+
const statusBar = blessed.box({
|
|
144
|
+
bottom: 0, left: 0, width: '100%', height: 3,
|
|
145
|
+
tags: true,
|
|
146
|
+
style: { bg: '#111111', fg: '#888888' },
|
|
147
|
+
border: { type: 'line', fg: '#222222' }
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Left: workspace info
|
|
151
|
+
const statusLeft = blessed.text({
|
|
152
|
+
parent: statusBar,
|
|
153
|
+
top: 0, left: 1,
|
|
154
|
+
width: '33%',
|
|
155
|
+
height: 1,
|
|
156
|
+
tags: true,
|
|
157
|
+
content: ` workspace {bold}(${workspaceName}){/bold}`,
|
|
158
|
+
style: { bg: '#111111', fg: '#888888' }
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Center: sandbox status
|
|
162
|
+
const statusCenter = blessed.text({
|
|
163
|
+
parent: statusBar,
|
|
164
|
+
top: 0,
|
|
165
|
+
left: 'center',
|
|
166
|
+
width: '34%',
|
|
167
|
+
height: 1,
|
|
168
|
+
align: 'center',
|
|
169
|
+
tags: true,
|
|
170
|
+
content: `{#cc4444-fg}no sandbox{/}`,
|
|
171
|
+
style: { bg: '#111111', fg: '#888888' }
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Right: current model
|
|
175
|
+
const statusRight = blessed.text({
|
|
176
|
+
parent: statusBar,
|
|
177
|
+
top: 0, right: 1,
|
|
178
|
+
width: '33%',
|
|
179
|
+
height: 1,
|
|
180
|
+
align: 'right',
|
|
181
|
+
tags: true,
|
|
182
|
+
content: `{#88e0b0-fg}${modelName}{/}`,
|
|
183
|
+
style: { bg: '#111111', fg: '#888888' }
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
function updateStatusBar(thinkingText = null) {
|
|
187
|
+
if (thinkingText) {
|
|
188
|
+
statusCenter.setContent(`{#88e0b0-fg}${thinkingText}{/}`);
|
|
189
|
+
} else {
|
|
190
|
+
statusCenter.setContent(`{#cc4444-fg}no sandbox{/}`);
|
|
191
|
+
}
|
|
192
|
+
screen.render();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Update model name in status bar (called after /models switch) */
|
|
196
|
+
function updateStatusModel(newModel) {
|
|
197
|
+
statusRight.setContent(`{#88e0b0-fg}${newModel}{/}`);
|
|
198
|
+
screen.render();
|
|
199
|
+
}
|
|
200
|
+
updateStatusBar();
|
|
201
|
+
|
|
202
|
+
// ─── Append widgets to screen ─────────────────────────────────────────────
|
|
203
|
+
screen.append(banner);
|
|
204
|
+
screen.append(divider1);
|
|
205
|
+
screen.append(chatBox);
|
|
206
|
+
screen.append(divider2);
|
|
207
|
+
screen.append(hintBar);
|
|
208
|
+
screen.append(inputBox);
|
|
209
|
+
screen.append(statusBar);
|
|
210
|
+
screen.append(placeholderWidget); // sibling on top of inputBox
|
|
211
|
+
|
|
212
|
+
// ─── Suggestion List ──────────────────────────────────────────────────────
|
|
213
|
+
const commandList = blessed.list({
|
|
214
|
+
parent: screen,
|
|
215
|
+
bottom: 6, // Above hintBar
|
|
216
|
+
left: 2,
|
|
217
|
+
width: '70%',
|
|
218
|
+
height: 8,
|
|
219
|
+
tags: true,
|
|
220
|
+
keys: false, // We will handle keys manually to keep focus on input
|
|
221
|
+
vi: false,
|
|
222
|
+
hidden: true,
|
|
223
|
+
border: { type: 'line', fg: '#88e0b0' },
|
|
224
|
+
style: {
|
|
225
|
+
bg: '#111111',
|
|
226
|
+
fg: '#ffffff',
|
|
227
|
+
selected: {
|
|
228
|
+
bg: '#334433',
|
|
229
|
+
fg: '#88e0b0',
|
|
230
|
+
bold: true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
let activeSuggestions = [];
|
|
236
|
+
|
|
237
|
+
function updateSuggestions(filter = '') {
|
|
238
|
+
activeSuggestions = SLASH_COMMANDS.filter(cmd =>
|
|
239
|
+
cmd.name.toLowerCase().startsWith(filter.toLowerCase())
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (activeSuggestions.length === 0) {
|
|
243
|
+
commandList.hide();
|
|
244
|
+
screen.render();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const items = activeSuggestions.map(cmd =>
|
|
249
|
+
` {bold}${cmd.name}{/} {gray-fg}${cmd.desc}{/}`
|
|
250
|
+
);
|
|
251
|
+
commandList.setItems(items);
|
|
252
|
+
commandList.select(0);
|
|
253
|
+
commandList.show();
|
|
254
|
+
commandList.setFront();
|
|
255
|
+
screen.render();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
// ─── Input events ─────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
// ─── Input events ─────────────────────────────────────────────────────────
|
|
262
|
+
let lastListVisible = false;
|
|
263
|
+
|
|
264
|
+
// Consolidated key handling
|
|
265
|
+
inputBox.on('element keypress', (el, ch, key) => {
|
|
266
|
+
// 1. Handle placeholder visibility
|
|
267
|
+
if (!key.ctrl && !key.meta && key.name !== 'enter' && key.name !== 'tab') {
|
|
268
|
+
if (ch) hidePlaceholder();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 2. Handle suggestion list navigation
|
|
272
|
+
if (!commandList.hidden) {
|
|
273
|
+
if (key.name === 'up') {
|
|
274
|
+
commandList.up();
|
|
275
|
+
screen.render();
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (key.name === 'down') {
|
|
279
|
+
commandList.down();
|
|
280
|
+
screen.render();
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
if (key.name === 'escape') {
|
|
284
|
+
commandList.hide();
|
|
285
|
+
lastListVisible = false;
|
|
286
|
+
screen.render();
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 3. Logic for suggestions and placeholder after key is processed
|
|
292
|
+
setImmediate(() => {
|
|
293
|
+
const val = (inputBox.getValue ? inputBox.getValue() : inputBox.value) || '';
|
|
294
|
+
const isCommand = val.startsWith('/') && !val.includes(' ');
|
|
295
|
+
|
|
296
|
+
// Only render if visibility changed or list is updated
|
|
297
|
+
if (isCommand) {
|
|
298
|
+
updateSuggestions(val);
|
|
299
|
+
lastListVisible = true;
|
|
300
|
+
} else if (lastListVisible) {
|
|
301
|
+
commandList.hide();
|
|
302
|
+
lastListVisible = false;
|
|
303
|
+
screen.render();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!val.trim()) {
|
|
307
|
+
showPlaceholder();
|
|
308
|
+
} else {
|
|
309
|
+
hidePlaceholder();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
// Submit or Select Suggestion on Enter
|
|
316
|
+
inputBox.key(['enter'], () => {
|
|
317
|
+
if (!commandList.hidden) {
|
|
318
|
+
const selected = activeSuggestions[commandList.selected];
|
|
319
|
+
if (selected) {
|
|
320
|
+
inputBox.setValue(selected.name + ' ');
|
|
321
|
+
commandList.hide();
|
|
322
|
+
hidePlaceholder();
|
|
323
|
+
inputBox.focus();
|
|
324
|
+
screen.render();
|
|
325
|
+
return; // Don't submit yet, let user add args or press enter again
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const raw = (inputBox.getValue ? inputBox.getValue() : inputBox.value) || '';
|
|
330
|
+
const text = raw.trim();
|
|
331
|
+
if (!text) return;
|
|
332
|
+
|
|
333
|
+
// Clear input and restore placeholder
|
|
334
|
+
inputBox.clearValue();
|
|
335
|
+
showPlaceholder();
|
|
336
|
+
inputBox.focus();
|
|
337
|
+
screen.render();
|
|
338
|
+
|
|
339
|
+
if (text.toLowerCase() === 'exit' || text.toLowerCase() === 'quit') {
|
|
340
|
+
onExit();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
onSubmit(text);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Shift+Enter = newline in input
|
|
348
|
+
inputBox.key(['S-enter'], () => {
|
|
349
|
+
hidePlaceholder();
|
|
350
|
+
const val = (inputBox.getValue ? inputBox.getValue() : inputBox.value) || '';
|
|
351
|
+
inputBox.setValue(val + '\n');
|
|
352
|
+
screen.render();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Ctrl+C — double-press to exit
|
|
356
|
+
let ctrlCPressed = false;
|
|
357
|
+
let ctrlCTimer = null;
|
|
358
|
+
const HINT_DEFAULT = `{gray-fg} Ctrl+Y copy last response · /help for commands{/}`;
|
|
359
|
+
|
|
360
|
+
screen.key(['C-c'], () => {
|
|
361
|
+
if (ctrlCPressed) {
|
|
362
|
+
clearTimeout(ctrlCTimer);
|
|
363
|
+
onExit();
|
|
364
|
+
} else {
|
|
365
|
+
ctrlCPressed = true;
|
|
366
|
+
hintBar.setContent(`{bold}{yellow-fg} Press Ctrl+C again to exit.{/} {gray-fg}(or type 'exit'){/}`);
|
|
367
|
+
screen.render();
|
|
368
|
+
ctrlCTimer = setTimeout(() => {
|
|
369
|
+
ctrlCPressed = false;
|
|
370
|
+
hintBar.setContent(HINT_DEFAULT);
|
|
371
|
+
screen.render();
|
|
372
|
+
}, 2000);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// ESC — exit immediately
|
|
377
|
+
screen.key(['escape'], () => {
|
|
378
|
+
onExit();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ─── Clipboard copy (Ctrl+Y) ──────────────────────────────────────────────
|
|
382
|
+
function copyToClipboard(text) {
|
|
383
|
+
// Try xclip first, then xsel as fallback
|
|
384
|
+
const tools = [
|
|
385
|
+
`echo ${JSON.stringify(text)} | xclip -selection clipboard`,
|
|
386
|
+
`echo ${JSON.stringify(text)} | xsel --clipboard --input`
|
|
387
|
+
];
|
|
388
|
+
for (const cmd of tools) {
|
|
389
|
+
try {
|
|
390
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
391
|
+
return true;
|
|
392
|
+
} catch (_) {}
|
|
393
|
+
}
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function flashHint(msg, durationMs = 2000) {
|
|
398
|
+
hintBar.setContent(msg);
|
|
399
|
+
screen.render();
|
|
400
|
+
setTimeout(() => {
|
|
401
|
+
hintBar.setContent(HINT_DEFAULT);
|
|
402
|
+
screen.render();
|
|
403
|
+
}, durationMs);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
screen.key(['C-y'], () => {
|
|
407
|
+
if (!lastAssistantResponse) {
|
|
408
|
+
flashHint(`{yellow-fg} No response to copy yet.{/}`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const ok = copyToClipboard(lastAssistantResponse);
|
|
412
|
+
if (ok) {
|
|
413
|
+
flashHint(`{#88e0b0-fg} ✓ Copied to clipboard!{/}`);
|
|
414
|
+
} else {
|
|
415
|
+
flashHint(`{red-fg} ✖ Copy failed. Install xclip: sudo apt install xclip{/}`, 3000);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ─── Initial render ───────────────────────────────────────────────────────
|
|
420
|
+
inputBox.focus();
|
|
421
|
+
screen.render();
|
|
422
|
+
|
|
423
|
+
// ─── Public API ───────────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
// Track last assistant response for clipboard copy
|
|
426
|
+
let lastAssistantResponse = '';
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* @param {'user'|'assistant'|'system'|'error'} role
|
|
430
|
+
* @param {string} text
|
|
431
|
+
*/
|
|
432
|
+
function appendMessage(role, text) {
|
|
433
|
+
const lines = text.split('\n');
|
|
434
|
+
if (role === 'user') {
|
|
435
|
+
chatBox.log(`\n {bold}{#88e0b0-fg}>{/} {#ffffff-fg}${lines[0]}{/}`);
|
|
436
|
+
lines.slice(1).forEach(l => chatBox.log(` {#ffffff-fg}${l}{/}`));
|
|
437
|
+
} else if (role === 'assistant') {
|
|
438
|
+
lastAssistantResponse = text; // track for Ctrl+Y
|
|
439
|
+
chatBox.log(`\n {bold}{#d4a8ff-fg}Mint:{/} {#ffffff-fg}${lines[0]}{/}`);
|
|
440
|
+
lines.slice(1).forEach(l => chatBox.log(` {#ffffff-fg}${l}{/}`));
|
|
441
|
+
chatBox.log('');
|
|
442
|
+
} else if (role === 'system') {
|
|
443
|
+
chatBox.log(`\n {gray-fg}${text}{/}`);
|
|
444
|
+
} else if (role === 'error') {
|
|
445
|
+
chatBox.log(`\n {red-fg}✖ ${text}{/}`);
|
|
446
|
+
}
|
|
447
|
+
screen.render();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Show/hide thinking indicator in status bar */
|
|
451
|
+
function setThinking(active, secondsElapsed = 0) {
|
|
452
|
+
if (active) {
|
|
453
|
+
updateStatusBar(`Thinking... {gray-fg}(esc to cancel, ${secondsElapsed}s){/}`);
|
|
454
|
+
} else {
|
|
455
|
+
updateStatusBar(null);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Copy last assistant response to clipboard */
|
|
460
|
+
function copyLastResponse() {
|
|
461
|
+
if (!lastAssistantResponse) return false;
|
|
462
|
+
return copyToClipboard(lastAssistantResponse);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return { screen, appendMessage, setThinking, updateStatusModel, copyLastResponse };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
module.exports = { createChatUI };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const colors = {
|
|
5
|
+
reset: "\x1b[0m",
|
|
6
|
+
bright: "\x1b[1m",
|
|
7
|
+
mint: "\x1b[38;5;121m",
|
|
8
|
+
pink: "\x1b[38;5;213m",
|
|
9
|
+
cyan: "\x1b[36m",
|
|
10
|
+
yellow: "\x1b[33m"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function displayFeatures() {
|
|
14
|
+
console.log(`\n${colors.mint}${colors.bright}Mint Feature & Command List${colors.reset}\n`);
|
|
15
|
+
|
|
16
|
+
console.log(`${colors.bright}Quick Setup:${colors.reset}`);
|
|
17
|
+
console.log(` - ${colors.cyan}mint onboard${colors.reset} : Setup API Key and Model`);
|
|
18
|
+
console.log(` - ${colors.cyan}mint onboard --daemon${colors.reset} : Install background agent (Linux)`);
|
|
19
|
+
console.log(` - ${colors.cyan}mint chat${colors.reset} : Start chatting (or just type "mint")`);
|
|
20
|
+
|
|
21
|
+
console.log(`\n${colors.bright}CLI Commands:${colors.reset}`);
|
|
22
|
+
const commands = [
|
|
23
|
+
{ cmd: 'mint', desc: 'Start interactive chat session (Default)' },
|
|
24
|
+
{ cmd: 'mint onboard', desc: 'Run setup wizard (API Key, Model, Daemon)' },
|
|
25
|
+
{ cmd: 'mint agent', desc: 'Run Mint as a background agent (Headless)' },
|
|
26
|
+
{ cmd: 'mint list', desc: 'Show this features & commands list' }
|
|
27
|
+
];
|
|
28
|
+
commands.forEach(c => console.log(` - ${colors.cyan}${c.cmd.padEnd(15)}${colors.reset} : ${c.desc}`));
|
|
29
|
+
|
|
30
|
+
console.log(`\n${colors.bright}AI Core Actions (Automation):${colors.reset}`);
|
|
31
|
+
const actions = [
|
|
32
|
+
{ act: 'open_url', desc: 'Open any website or search Google' },
|
|
33
|
+
{ act: 'open_app', desc: 'Launch desktop applications' },
|
|
34
|
+
{ act: 'file_ops', desc: 'Create folders, Open files, Delete/Trash items' },
|
|
35
|
+
{ act: 'knowledge', desc: 'Learn from files (PDF, Docx, TXT, MD, XLSX) or Web URLs' },
|
|
36
|
+
{ act: 'system', desc: 'Volume, Brightness, Mute, Sleep, Power Control' }
|
|
37
|
+
];
|
|
38
|
+
actions.forEach(a => console.log(` - ${colors.yellow}${a.act.padEnd(15)}${colors.reset} : ${a.desc}`));
|
|
39
|
+
|
|
40
|
+
console.log(`\n${colors.bright}Available Plugins:${colors.reset}`);
|
|
41
|
+
const pluginsDir = path.join(__dirname, '../Plugins');
|
|
42
|
+
try {
|
|
43
|
+
const files = fs.readdirSync(pluginsDir);
|
|
44
|
+
const plugins = files
|
|
45
|
+
.filter(f => f.endsWith('.js') && f !== 'plugin_manager.js')
|
|
46
|
+
.map(f => f.replace('.js', ''));
|
|
47
|
+
|
|
48
|
+
plugins.forEach(p => console.log(` - ${colors.pink}${p}${colors.reset}`));
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.log(' (Could not load plugins list)');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`\n${colors.mint}${colors.bright}Type "mint" to start exploring!${colors.reset}\n`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { displayFeatures };
|