omni-agent-cli 2.0.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/src/ui.js ADDED
@@ -0,0 +1,320 @@
1
+ import chalk from 'chalk';
2
+ import gradient from 'gradient-string';
3
+ import boxen from 'boxen';
4
+ import ora from 'ora';
5
+
6
+ // ─── Palette ──────────────────────────────────────────────────────────────────
7
+ const C = {
8
+ primary: chalk.hex('#00FFFF'),
9
+ secondary: chalk.hex('#FF00FF'),
10
+ accent: chalk.hex('#FFFF00'),
11
+ success: chalk.hex('#00FF88'),
12
+ error: chalk.hex('#FF4466'),
13
+ warning: chalk.hex('#FF8800'),
14
+ dim: chalk.hex('#556677'),
15
+ dimBright: chalk.hex('#778899'),
16
+ white: chalk.hex('#E8E8F0'),
17
+ tool: chalk.hex('#88AAFF'),
18
+ user: chalk.hex('#AAFFCC'),
19
+ ai: chalk.hex('#CCAAFF'),
20
+ think: chalk.hex('#AADDFF'),
21
+ lineNum: chalk.hex('#555577'),
22
+ added: chalk.hex('#00FF88'),
23
+ removed: chalk.hex('#FF4466'),
24
+ code: chalk.hex('#E8D5A3'),
25
+ };
26
+
27
+ const GRAD_BANNER = gradient(['#00FFFF', '#FF00FF', '#FFFF00']);
28
+ const GRAD_RESULT = gradient(['#00FF88', '#00FFFF']);
29
+ const GRAD_TOOL = gradient(['#88AAFF', '#FF00FF']);
30
+ const GRAD_STATS = gradient(['#FFFF00', '#FF8800', '#FF4466']);
31
+ const GRAD_THINK = gradient(['#4488FF', '#00FFCC']);
32
+ const GRAD_CONFIRM= gradient(['#FFAA00', '#FF6600']);
33
+
34
+ // ─── Banner ────────────────────────────────────────────────────────────────────
35
+ export async function displayBanner() {
36
+ console.clear();
37
+ const lines = [
38
+ ' ██████╗ ███╗ ███╗███╗ ██╗██╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗',
39
+ '██╔═══██╗████╗ ████║████╗ ██║██║ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝',
40
+ '██║ ██║██╔████╔██║██╔██╗██║██║ ███████║██║ ███╗█████╗ ██╔██╗██║ ██║ ',
41
+ '██║ ██║██║╚██╔╝██║██║╚████║██║ ██╔══██║██║ ██║██╔══╝ ██║╚████║ ██║ ',
42
+ '╚██████╔╝██║ ╚═╝ ██║██║ ╚███║███████╗██║ ██║╚██████╔╝███████╗██║ ╚███║ ██║ ',
43
+ ' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚══╝ ╚═╝ ',
44
+ ];
45
+ console.log('\n');
46
+ for (const line of lines) console.log(' ' + GRAD_BANNER(line));
47
+
48
+ const badges = [
49
+ chalk.bgHex('#00FFFF').black(' Claude '),
50
+ chalk.bgHex('#FF6600').black(' Qwen '),
51
+ chalk.bgHex('#00AA44').black(' Groq '),
52
+ chalk.bgHex('#0066FF').black(' Gemini '),
53
+ chalk.bgHex('#FF00AA').black(' DeepSeek '),
54
+ C.dim('+ любой'),
55
+ ];
56
+ console.log('\n ' + badges.join(' '));
57
+ console.log(C.dim('\n v3.0.0 · type /help for commands\n'));
58
+ }
59
+
60
+ // ─── Spinner ───────────────────────────────────────────────────────────────────
61
+ export function createSpinner(text = 'Thinking…') {
62
+ return ora({
63
+ text: C.dim(text),
64
+ spinner: { interval: 80, frames: [C.primary('◐'), C.secondary('◓'), C.accent('◑'), C.primary('◒')] },
65
+ prefixText: ' ',
66
+ });
67
+ }
68
+
69
+ // ─── Thinking display (Qwen-style bullets) ─────────────────────────────────────
70
+ export function displayThinking(bullets) {
71
+ console.log('');
72
+ for (const b of bullets) {
73
+ console.log(' ' + C.dim('•') + ' ' + C.think(b));
74
+ }
75
+ }
76
+
77
+ // ─── User message ──────────────────────────────────────────────────────────────
78
+ export function displayUserMessage(content) {
79
+ console.log('\n' + C.dim(' ┌─') + C.user(' YOU'));
80
+ for (const line of content.split('\n')) {
81
+ console.log(C.dim(' │ ') + C.white(line));
82
+ }
83
+ console.log(C.dim(' └─'));
84
+ }
85
+
86
+ // ─── Assistant message ─────────────────────────────────────────────────────────
87
+ export function displayMessage(role, content) {
88
+ if (!content?.trim()) return;
89
+ if (role === 'user') { displayUserMessage(content); return; }
90
+
91
+ console.log('\n' + C.ai(' ╔═ ') + GRAD_RESULT('◈ AGENT') + C.dim(' ' + '═'.repeat(44)));
92
+ const lines = content.split('\n');
93
+ let inCode = false;
94
+ let codeLang = '';
95
+
96
+ for (const line of lines) {
97
+ if (line.startsWith('```')) {
98
+ if (!inCode) {
99
+ inCode = true;
100
+ codeLang = line.slice(3).trim();
101
+ const ll = codeLang ? C.accent(` ${codeLang} `) : '';
102
+ console.log(C.dim(' ║ ') + chalk.bgHex('#1A1A2E').hex('#888888')(' ') + ll);
103
+ } else {
104
+ inCode = false;
105
+ console.log(C.dim(' ║ ') + chalk.hex('#334455')('─'.repeat(50)));
106
+ }
107
+ continue;
108
+ }
109
+ if (inCode) {
110
+ console.log(C.dim(' ║ ') + chalk.bgHex('#0D0D1A')(' ') + C.code(line));
111
+ continue;
112
+ }
113
+ if (line.startsWith('### ')) { console.log(C.dim(' ║\n ║ ') + C.accent('▶ ') + chalk.bold.hex('#FFDD88')(line.slice(4))); continue; }
114
+ if (line.startsWith('## ')) { console.log(C.dim(' ║\n ║ ') + C.secondary('◆ ') + chalk.bold.hex('#FF88FF')(line.slice(3))); continue; }
115
+ if (line.startsWith('# ')) { console.log(C.dim(' ║\n ║ ') + C.primary('◈ ') + chalk.bold.hex('#00FFFF')(line.slice(2))); continue; }
116
+
117
+ let rendered = line
118
+ .replace(/\*\*(.+?)\*\*/g, (_, m) => chalk.bold.hex('#FFFAAA')(m))
119
+ .replace(/`(.+?)`/g, (_, m) => chalk.bgHex('#1A1A2E').hex('#88FFCC')(m));
120
+
121
+ if (/^[-*] /.test(line)) rendered = C.primary(' • ') + C.white(rendered.slice(2));
122
+ else if (/^\d+\. /.test(line)) rendered = C.accent(' ') + C.white(rendered);
123
+ else rendered = C.white(rendered);
124
+
125
+ console.log(C.dim(' ║ ') + rendered);
126
+ }
127
+ console.log(C.ai(' ╚' + '═'.repeat(52)));
128
+ }
129
+
130
+ // ─── Tool icons ────────────────────────────────────────────────────────────────
131
+ const TOOL_ICONS = {
132
+ list_directory: '📂', read_file: '📖',
133
+ write_file: '✍️ ', append_to_file: '📝',
134
+ patch_file: '🔧', delete_path: '🗑️ ',
135
+ copy_path: '📋', move_path: '🚚',
136
+ create_directory:'📁', get_file_info: '🔍',
137
+ find_files: '🔎', search_in_files: '🕵️ ',
138
+ execute_command: '⚡',
139
+ };
140
+
141
+ const TOOL_LABELS = {
142
+ write_file: 'WriteFile',
143
+ patch_file: 'PatchFile',
144
+ append_to_file: 'AppendFile',
145
+ delete_path: 'DeletePath',
146
+ move_path: 'MovePath',
147
+ execute_command: 'RunCommand',
148
+ read_file: 'ReadFile',
149
+ list_directory: 'ListDir',
150
+ find_files: 'FindFiles',
151
+ search_in_files: 'SearchFiles',
152
+ create_directory:'MkDir',
153
+ copy_path: 'CopyFile',
154
+ get_file_info: 'FileInfo',
155
+ };
156
+
157
+ // ─── File preview box (Qwen Coder style) ──────────────────────────────────────
158
+ export function displayFilePreview(toolName, input) {
159
+ const icon = TOOL_ICONS[toolName] || '🔷';
160
+ const tlabel = TOOL_LABELS[toolName] || toolName;
161
+ const target = input.path || input.source || input.command || '';
162
+ const content = input.content || input.new_text || input.command || '';
163
+
164
+ // Header line: "? WriteFile Writing to filename.js ←"
165
+ console.log('\n' + C.dim(' ?') + ' ' + GRAD_CONFIRM(tlabel) + ' ' + C.dim(getActionLabel(toolName, input)));
166
+
167
+ if (!content) return;
168
+
169
+ // Split into lines, show last N with line numbers
170
+ const allLines = content.split('\n');
171
+ const MAX_PREVIEW = 25;
172
+ const startLine = Math.max(0, allLines.length - MAX_PREVIEW);
173
+ const showLines = allLines.slice(startLine);
174
+
175
+ // Show "... N lines hidden ..." if truncated
176
+ if (startLine > 0) {
177
+ console.log(C.dim(` ... ${startLine} lines hidden ...`));
178
+ }
179
+
180
+ // Code preview with line numbers
181
+ const boxLines = showLines.map((line, i) => {
182
+ const lineNo = String(startLine + i + 1).padStart(4);
183
+ return C.lineNum(lineNo) + C.dim('\t') + C.code(line.length > 100 ? line.slice(0, 100) + '…' : line);
184
+ });
185
+
186
+ for (const l of boxLines) console.log(' ' + l);
187
+ }
188
+
189
+ function getActionLabel(toolName, input) {
190
+ switch (toolName) {
191
+ case 'write_file': return `Writing to ${input.path} ←`;
192
+ case 'patch_file': return `Patching ${input.path} ←`;
193
+ case 'append_to_file': return `Appending to ${input.path} ←`;
194
+ case 'delete_path': return `Deleting ${input.path} ⚠`;
195
+ case 'move_path': return `${input.source} → ${input.destination}`;
196
+ case 'execute_command': return `$ ${(input.command || '').slice(0, 60)}`;
197
+ default: return input.path || '';
198
+ }
199
+ }
200
+
201
+ // ─── Confirmation prompt (Qwen Coder style) ────────────────────────────────────
202
+ // Returns: 'yes' | 'always' | 'skip' | 'modify'
203
+ export async function displayConfirmPrompt(toolName) {
204
+ const isDestructive = toolName === 'delete_path';
205
+ const label = isDestructive
206
+ ? chalk.bold.red(' Apply this DESTRUCTIVE change?')
207
+ : chalk.bold.hex('#FFAA00')(' Apply this change?');
208
+
209
+ console.log('\n' + label + '\n');
210
+ console.log(C.success(' ● 1. Yes, allow once'));
211
+ console.log(C.dimBright(' 2. Yes, allow always'));
212
+ console.log(C.dimBright(' 3. No, skip this step'));
213
+ console.log(C.dimBright(' 4. No, suggest changes'));
214
+ console.log('\n' + C.dim(' Press ctrl-s to show more lines'));
215
+ }
216
+
217
+ // ─── Tool call (non-confirm) display ──────────────────────────────────────────
218
+ export function displayToolCall(toolName, input) {
219
+ const icon = TOOL_ICONS[toolName] || '🔷';
220
+ const label = GRAD_TOOL(toolName);
221
+ console.log('\n' + C.dim(' ┌') + C.tool('─ TOOL ─ ') + icon + ' ' + label);
222
+ const entries = Object.entries(input).slice(0, 3);
223
+ for (const [k, v] of entries) {
224
+ const val = typeof v === 'string' ? v.slice(0, 80) + (v.length > 80 ? '…' : '') : JSON.stringify(v).slice(0, 80);
225
+ console.log(C.dim(' │ ') + C.dim(`${k}: `) + chalk.hex('#AACCFF')(val));
226
+ }
227
+ console.log(C.dim(' └' + '─'.repeat(50)));
228
+ }
229
+
230
+ export function displayToolResult(toolName, result, durationMs) {
231
+ const preview = (result || '').toString().split('\n').slice(0, 6).join('\n');
232
+ const hasMore = (result || '').toString().split('\n').length > 6;
233
+ console.log(C.dim(' ┌') + C.success('─ RESULT ') + C.dim(`${durationMs}ms`));
234
+ for (const line of preview.split('\n')) {
235
+ console.log(C.dim(' │ ') + chalk.hex('#88EEB8')(line.slice(0, 110)));
236
+ }
237
+ if (hasMore) console.log(C.dim(' │ ') + C.dim(' … (truncated)'));
238
+ console.log(C.dim(' └' + '─'.repeat(50)));
239
+ }
240
+
241
+ export function displayError(message) {
242
+ console.log('\n' + boxen(C.error('⚠ ' + message), {
243
+ padding: { left: 2, right: 2, top: 0, bottom: 0 },
244
+ borderStyle: 'round', borderColor: 'red',
245
+ }));
246
+ }
247
+
248
+ // ─── Session stats ─────────────────────────────────────────────────────────────
249
+ export async function displayStats(stats) {
250
+ console.log('\n');
251
+ console.log(GRAD_STATS(' ╔═════════════════════════════════════════════════════╗'));
252
+ console.log(GRAD_STATS(' ║') + chalk.bold.white(' 📊 SESSION STATISTICS ') + GRAD_STATS('║'));
253
+ console.log(GRAD_STATS(' ╠═════════════════════════════════════════════════════╣'));
254
+
255
+ const row = (label, value, color = C.white) => {
256
+ const l = C.dim(label.padEnd(26));
257
+ const v = color(String(value));
258
+ console.log(GRAD_STATS(' ║ ') + l + v + ' '.repeat(Math.max(0, 24 - String(value).length)) + GRAD_STATS(' ║'));
259
+ };
260
+
261
+ row('⏱ Duration', stats.duration, C.accent);
262
+ row('💬 Messages', `${stats.messages.user} sent / ${stats.messages.assistant} got`, C.primary);
263
+ row('🌐 API Requests', stats.apiRequests, C.secondary);
264
+ row('🪙 Tokens In', stats.tokens.input.toLocaleString(), C.success);
265
+ row('🪙 Tokens Out', stats.tokens.output.toLocaleString(), C.success);
266
+ row('🪙 Total Tokens', stats.totalTokens.toLocaleString(), chalk.bold.hex('#AAFFAA'));
267
+ row('⚡ Tools Called', stats.totalToolCalls, C.tool);
268
+ row('💰 Est. Cost', `~$${stats.estimatedCost}`, C.warning);
269
+
270
+ if (stats.toolCallsList.length > 0) {
271
+ console.log(GRAD_STATS(' ╠─────────────────────────────────────────────────────╣'));
272
+ console.log(GRAD_STATS(' ║ ') + C.dim('Tool Usage:') + ' '.repeat(43) + GRAD_STATS(' ║'));
273
+ for (const [tool, count] of stats.toolCallsList.slice(0, 8)) {
274
+ const icon = TOOL_ICONS[tool] || '•';
275
+ const bar = '█'.repeat(Math.min(count * 2, 15));
276
+ const label = `${icon} ${tool}`.padEnd(28);
277
+ console.log(GRAD_STATS(' ║ ') + C.dim(label) + C.primary(bar) + C.accent(` ${count}`) + ' '.repeat(Math.max(0, 16 - bar.length)) + GRAD_STATS(' ║'));
278
+ }
279
+ }
280
+
281
+ console.log(GRAD_STATS(' ╚═════════════════════════════════════════════════════╝'));
282
+ console.log('\n' + C.dim(' Thanks for using OmniAgent! ✨') + '\n');
283
+ }
284
+
285
+ // ─── Help ──────────────────────────────────────────────────────────────────────
286
+ export function displayHelp() {
287
+ const help = boxen(
288
+ [
289
+ C.primary('COMMANDS'),
290
+ '',
291
+ ` ${C.accent('/help')} ${C.dim('Show this help')}`,
292
+ ` ${C.accent('/clear')} ${C.dim('Clear screen + reset history')}`,
293
+ ` ${C.accent('/stats')} ${C.dim('Show session stats')}`,
294
+ ` ${C.accent('/cd <dir>')} ${C.dim('Change working directory')}`,
295
+ ` ${C.accent('/model <name>')} ${C.dim('Switch model on the fly')}`,
296
+ ` ${C.accent('/models')} ${C.dim('Fetch live models (current provider)')}`,
297
+ ` ${C.accent('/models <provider>')} ${C.dim('Fetch models for provider')}`,
298
+ ` ${C.accent('/providers')} ${C.dim('List all providers')}`,
299
+ ` ${C.accent('/setup')} ${C.dim('Reconfigure provider / API key')}`,
300
+ ` ${C.accent('/confirm on|off')} ${C.dim('Toggle confirmation prompts')}`,
301
+ ` ${C.accent('/reset')} ${C.dim('New conversation')}`,
302
+ ` ${C.accent('/exit')} ${C.dim('Exit + show stats')}`,
303
+ '',
304
+ C.primary('CONFIRMATION (Qwen Coder style)'),
305
+ '',
306
+ ` ${C.dim('Before write/edit/delete/run — agent shows preview')}`,
307
+ ` ${C.success('1')} ${C.dim('Yes, once')} ${C.success('2')} ${C.dim('Yes, always')} ${C.warning('3')} ${C.dim('Skip')} ${C.error('4')} ${C.dim('Suggest changes')}`,
308
+ '',
309
+ C.primary('AGENT TOOLS'),
310
+ '',
311
+ ` ${C.success('📂')} read/write/patch/move/copy/delete/mkdir`,
312
+ ` ${C.success('🔎')} find files by glob, grep in files`,
313
+ ` ${C.success('⚡')} execute any shell command`,
314
+ ].join('\n'),
315
+ { padding: 1, borderStyle: 'double', borderColor: 'cyan', title: '⟨ OMNI-AGENT v3 ⟩', titleAlignment: 'center' }
316
+ );
317
+ console.log('\n' + help + '\n');
318
+ }
319
+
320
+ export { C };