helix-assistant-cli 0.2.14 → 0.6.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.
@@ -1,4 +1,4 @@
1
- import chalk from 'chalk';
1
+ import chalk from 'chalk';
2
2
  import type { AgentClient } from '../client/agent-client';
3
3
  import { formatSkill, formatMcp, ORANGE, CYAN, MAGENTA, DIM } from './colors';
4
4
  import { discoverUserSkillNames, getUserSkillsDirectory } from '../skills/user-skill-discovery';
@@ -14,16 +14,19 @@ export const SLASH_COMMANDS: CommandDefinition[] = [
14
14
  { command: '/skill', description: 'Listar o inspeccionar skills registradas', usage: '/skill [nombre]' },
15
15
  { command: '/mcp', description: 'Listar servidores MCP y sus herramientas', usage: '/mcp [defaults]' },
16
16
  { command: '/create-mcp', description: 'Registrar y configurar un nuevo servidor MCP' },
17
- { command: '/browser', description: 'Ver estado del navegador web o abrir URLs', usage: '/browser [open <url>]' },
17
+ { command: '/browser', description: 'Ver estado, abrir URLs o alternar la vista ASCII', usage: '/browser [open <url>|live [on|off]]' },
18
+ { command: '/subagents', description: 'Ver el panel ASCII o chatear con un subagente', usage: '/subagents [list|chat <id> <mensaje>]' },
18
19
  { command: '/config-apikey', description: 'Configurador interactivo de API Keys (OpenAI, Anthropic, Gemini, etc.)' },
19
20
  { command: '/show-apikey', description: 'Ver estado y validez de las API Keys configuradas' },
20
- { command: '/microphone', description: 'Ver o alternar estado del micrófono', usage: '/microphone [enable|disable]' },
21
+ { command: '/microphone', description: 'Ver o alternar estado del micrófono', usage: '/microphone [enable|disable]' },
21
22
  { command: '/tasks', description: 'Ver tareas activas y recientes del agente' },
23
+ { command: '/model', description: 'Ver o cambiar el modelo activo (free/local/cloud)', usage: '/model [list|set <id>|effort <low|medium|high>]' },
22
24
  { command: '/models', description: 'Listar modelos disponibles de proveedores configurados' },
25
+ { command: '/memory', description: 'Ver estadísticas del harness de aprendizaje adaptativo', usage: '/memory [stats|rules|reset]' },
23
26
  { command: '/status', description: 'Mostrar resumen del estado del sistema' },
24
27
  { command: '/providers', description: 'Ver proveedores configurados y salud de sus claves' },
25
28
  { command: '/capabilities', description: 'Ver capacidades activas: sandbox, subagentes, voz y browser' },
26
- { command: '/settings', description: 'Ver configuración efectiva sin exponer secretos' },
29
+ { command: '/settings', description: 'Ver configuración efectiva sin exponer secretos' },
27
30
  { command: '/clear', description: 'Limpiar pantalla y restaurar el banner de HELIX' },
28
31
  { command: '/stop', description: 'Parada de emergencia de todas las tareas activas' },
29
32
  { command: '/help', description: 'Mostrar ayuda de comandos y atajos' },
@@ -116,7 +119,21 @@ export class Autocompleter {
116
119
  return [hits.length ? hits : options, line];
117
120
  }
118
121
 
119
- // 3. Browser sub-arguments
122
+ // 3. Model sub-arguments
123
+ if (trimmed.startsWith('/model ')) {
124
+ const options = ['/model list', '/model set ', '/model effort low', '/model effort medium', '/model effort high'];
125
+ const hits = options.filter((o) => o.toLowerCase().startsWith(trimmed.toLowerCase()));
126
+ return [hits.length ? hits : options, line];
127
+ }
128
+
129
+ // 3b. Memory sub-arguments
130
+ if (trimmed.startsWith('/memory ')) {
131
+ const options = ['/memory stats', '/memory rules', '/memory reset'];
132
+ const hits = options.filter((o) => o.toLowerCase().startsWith(trimmed.toLowerCase()));
133
+ return [hits.length ? hits : options, line];
134
+ }
135
+
136
+ // 4. Browser sub-arguments
120
137
  if (trimmed.startsWith('/browser ')) {
121
138
  const options = ['/browser open https://'];
122
139
  return [options, line];
@@ -158,16 +175,16 @@ export class Autocompleter {
158
175
  */
159
176
  public renderHelp(): void {
160
177
  console.log('');
161
- console.log(` ${chalk.hex('#FF8C00').bold('⬡ Comandos Disponibles')} ${chalk.gray('(/ + [Tab] para autocompletar)')}`);
162
- console.log(` ${chalk.gray('─'.repeat(50))}`);
178
+ console.log(` ${chalk.hex('#FF8C00').bold('⬡ Comandos Disponibles')} ${chalk.gray('(/ + [Tab] para autocompletar)')}`);
179
+ console.log(` ${chalk.gray('─'.repeat(50))}`);
163
180
  for (const cmd of SLASH_COMMANDS) {
164
181
  const usage = cmd.usage ? ` ${chalk.gray(cmd.usage)}` : '';
165
182
  console.log(` ${chalk.magenta(cmd.command.padEnd(16))} ${chalk.gray(cmd.description)}${usage}`);
166
183
  }
167
184
 
168
185
  console.log('');
169
- console.log(` ${chalk.hex('#FF8C00').bold('⬡ Prefijos Especiales')}`);
170
- console.log(` ${chalk.gray('─'.repeat(50))}`);
186
+ console.log(` ${chalk.hex('#FF8C00').bold('⬡ Prefijos Especiales')}`);
187
+ console.log(` ${chalk.gray('─'.repeat(50))}`);
171
188
  console.log(` ${formatSkill('nombre')} ${chalk.gray('Invoca una skill registrada')}`);
172
189
  console.log(` ${formatMcp('herramienta')} ${chalk.gray('Invoca herramientas MCP')}`);
173
190
  console.log('');
@@ -0,0 +1,82 @@
1
+ import jpeg from 'jpeg-js';
2
+
3
+ const ASCII_RAMP = ' .:-=+*#%@';
4
+ const MAX_FRAME_WIDTH = 76;
5
+ const MAX_FRAME_HEIGHT = 34;
6
+ const MAX_JPEG_BYTES = 560_000;
7
+
8
+ export function grayscaleToAscii(
9
+ pixels: number[][],
10
+ width = MAX_FRAME_WIDTH,
11
+ ): string {
12
+ if (!pixels.length || !pixels[0]?.length) {
13
+ throw new Error('Browser frame has no pixels to render.');
14
+ }
15
+ const boundedWidth = Math.max(1, Math.min(MAX_FRAME_WIDTH, Math.floor(width)));
16
+ const sourceHeight = pixels.length;
17
+ const sourceWidth = pixels[0].length;
18
+ const outputHeight = Math.max(
19
+ 1,
20
+ Math.min(MAX_FRAME_HEIGHT, Math.round((sourceHeight / sourceWidth) * boundedWidth * 0.5)),
21
+ );
22
+ const lines: string[] = [];
23
+
24
+ for (let y = 0; y < outputHeight; y += 1) {
25
+ const sourceY = Math.min(sourceHeight - 1, Math.floor((y / outputHeight) * sourceHeight));
26
+ let line = '';
27
+ for (let x = 0; x < boundedWidth; x += 1) {
28
+ const sourceX = Math.min(sourceWidth - 1, Math.floor((x / boundedWidth) * sourceWidth));
29
+ const value = Math.max(0, Math.min(255, pixels[sourceY][sourceX]));
30
+ const index = Math.round(((255 - value) / 255) * (ASCII_RAMP.length - 1));
31
+ line += ASCII_RAMP[index];
32
+ }
33
+ lines.push(line);
34
+ }
35
+ return lines.join('\n');
36
+ }
37
+
38
+ export function renderJpegAsAscii(base64: string, width = MAX_FRAME_WIDTH): string {
39
+ if (
40
+ typeof base64 !== 'string'
41
+ || !/^[A-Za-z0-9+/]+={0,2}$/.test(base64)
42
+ || base64.length > Math.ceil(MAX_JPEG_BYTES * 4 / 3)
43
+ ) {
44
+ throw new Error('Browser frame is not a valid, bounded Base64 JPEG.');
45
+ }
46
+ const bytes = Buffer.from(base64, 'base64');
47
+ if (bytes.length === 0 || bytes.length > MAX_JPEG_BYTES) {
48
+ throw new Error('Browser frame exceeds the supported JPEG size.');
49
+ }
50
+ const image = jpeg.decode(bytes, {
51
+ useTArray: true,
52
+ formatAsRGBA: true,
53
+ maxResolutionInMP: 4,
54
+ maxMemoryUsageInMB: 128,
55
+ });
56
+ const pixels: number[][] = [];
57
+ const outputWidth = Math.min(MAX_FRAME_WIDTH, image.width);
58
+ const outputHeight = Math.max(
59
+ 1,
60
+ Math.min(
61
+ MAX_FRAME_HEIGHT,
62
+ Math.round((image.height / image.width) * outputWidth * 0.5),
63
+ ),
64
+ );
65
+
66
+ for (let y = 0; y < outputHeight; y += 1) {
67
+ const row: number[] = [];
68
+ const sourceY = Math.min(image.height - 1, Math.floor((y / outputHeight) * image.height));
69
+ for (let x = 0; x < outputWidth; x += 1) {
70
+ const sourceX = Math.min(image.width - 1, Math.floor((x / outputWidth) * image.width));
71
+ const offset = (sourceY * image.width + sourceX) * 4;
72
+ row.push(
73
+ image.data[offset] * 0.299
74
+ + image.data[offset + 1] * 0.587
75
+ + image.data[offset + 2] * 0.114,
76
+ );
77
+ }
78
+ pixels.push(row);
79
+ }
80
+
81
+ return grayscaleToAscii(pixels, Math.min(width, outputWidth));
82
+ }
package/src/ui/repl.ts CHANGED
@@ -1,4 +1,4 @@
1
- import readline from 'readline';
1
+ import readline from 'readline';
2
2
  import chalk from 'chalk';
3
3
  import prompts from 'prompts';
4
4
  import { AgentClient } from '../client/agent-client';
@@ -24,9 +24,13 @@ import { handleCreateSkill, handleListSkills } from '../commands/skills';
24
24
  import { handleCreateMcp, handleListMcp, handleMcpDefaults } from '../commands/mcp';
25
25
  import { handleMicrophoneCommand } from '../commands/mic';
26
26
  import { handleBrowserCommand } from '../commands/browser';
27
+ import { handleSubagentsCommand } from '../commands/subagents';
27
28
  import type { ConfirmationRequest } from '../client/types';
28
29
  import { formatTerminalMarkdown } from './markdown';
29
30
  import { handleCapabilities, handleProviders, handleSettings } from '../commands/diagnostics';
31
+ import { renderJpegAsAscii } from './browser-ascii';
32
+ import { handleModelCommand } from '../commands/model';
33
+ import { handleMemoryCommand } from '../commands/memory';
30
34
 
31
35
  export interface ReplOptions {
32
36
  wsUrl: string;
@@ -47,6 +51,7 @@ export class HelixRepl {
47
51
  private activeTasksCount: number = 0;
48
52
  private isProcessingPrompt: boolean = false;
49
53
  private plain: boolean = false;
54
+ private browserLiveViewEnabled: boolean = false;
50
55
 
51
56
  constructor(private options: ReplOptions) {
52
57
  this.conversationId = `cli_conv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
@@ -116,7 +121,7 @@ export class HelixRepl {
116
121
  this.waitingLogged = false;
117
122
  await this._syncSettings();
118
123
  await this.autocompleter.refreshSuggestions();
119
- process.stdout.write(`\r\x1b[K${chalk.green('●')} ${chalk.greenBright.bold('Conectado al agente HELIX')} \n`);
124
+ process.stdout.write(`\r\x1b[K${chalk.green('●')} ${chalk.greenBright.bold('Conectado al agente HELIX')} \n`);
120
125
  this._promptUser();
121
126
  });
122
127
 
@@ -126,39 +131,39 @@ export class HelixRepl {
126
131
  }
127
132
  if (!this.waitingLogged) {
128
133
  this.waitingLogged = true;
129
- console.log(`\n${RED('●')} ${chalk.yellowBright('Esperando al servicio del agente HELIX en ws://127.0.0.1:8765...')}`);
130
- console.log(`${GRAY(' (Si no responde tras 3 reintentos, el CLI lo iniciará automáticamente en segundo plano)')}\n`);
134
+ console.log(`\n${RED('●')} ${chalk.yellowBright('Esperando al servicio del agente HELIX en ws://127.0.0.1:8765...')}`);
135
+ console.log(`${GRAY(' (Si no responde tras 3 reintentos, el CLI lo iniciará automáticamente en segundo plano)')}\n`);
131
136
  }
132
137
  this._promptUser();
133
138
  });
134
139
 
135
140
  this.client.on('auto_spawning_agent', () => {
136
- process.stdout.write(`\n${CYAN_BOLD('⚙️')} ${chalk.hex('#FF9F1C').bold('[AUTO-START]:')} Iniciando el servicio del agente HELIX en segundo plano...\n`);
137
- process.stdout.write(`${GRAY(' Por favor espere mientras termina de inicializarse (no se realizarán reintentos hasta que esté listo).')}\n`);
141
+ process.stdout.write(`\n${CYAN_BOLD('⚙️')} ${chalk.hex('#FF9F1C').bold('[AUTO-START]:')} Iniciando el servicio del agente HELIX en segundo plano...\n`);
142
+ process.stdout.write(`${GRAY(' Por favor espere mientras termina de inicializarse (no se realizarán reintentos hasta que esté listo).')}\n`);
138
143
  });
139
144
 
140
145
  this.client.on('agent_starting_progress', (data: { elapsedSec: number }) => {
141
- process.stdout.write(`\r ${chalk.hex('#FF9F1C')('⏳')} ${chalk.gray(`Inicializando subsistemas de HELIX en segundo plano... (${data.elapsedSec}s)`)}`);
146
+ process.stdout.write(`\r ${chalk.hex('#FF9F1C')('⏳')} ${chalk.gray(`Inicializando subsistemas de HELIX en segundo plano... (${data.elapsedSec}s)`)}`);
142
147
  });
143
148
 
144
149
  this.client.on('agent_ready', () => {
145
- process.stdout.write(`\r\x1b[K ${chalk.green('✔')} ${chalk.greenBright('Servicio de HELIX listo. Conectando...')}\n\n`);
150
+ process.stdout.write(`\r\x1b[K ${chalk.green('✔')} ${chalk.greenBright('Servicio de HELIX listo. Conectando...')}\n\n`);
146
151
  });
147
152
 
148
153
  this.client.on('agent_start_failed', (data: { error?: string }) => {
149
- process.stdout.write(`\r\x1b[K\n ${chalk.red('✖')} ${chalk.redBright('No se pudo iniciar el servicio automáticamente:')} ${chalk.gray(data?.error || 'Tiempo agotado')}\n`);
154
+ process.stdout.write(`\r\x1b[K\n ${chalk.red('✖')} ${chalk.redBright('No se pudo iniciar el servicio automáticamente:')} ${chalk.gray(data?.error || 'Tiempo agotado')}\n`);
150
155
  this._promptUser();
151
156
  });
152
157
 
153
158
  // Voice Capture indicator
154
159
  this.client.on('voice_capture_started', () => {
155
160
  this.isCapturingVoice = true;
156
- process.stdout.write(`\n${chalk.bgRed.white.bold(' 🎙️ [VOZ DETECTADA - HEY HELIX]: Capturando audio... ')}\n`);
161
+ process.stdout.write(`\n${chalk.bgRed.white.bold(' 🎙️ [VOZ DETECTADA - HEY HELIX]: Capturando audio... ')}\n`);
157
162
  });
158
163
 
159
164
  this.client.on('voice_capture_stopped', () => {
160
165
  this.isCapturingVoice = false;
161
- process.stdout.write(`\n${chalk.bgCyan.black.bold(' 🔊 [AUDIO RECIBIDO]: Procesando voz... ')}\n`);
166
+ process.stdout.write(`\n${chalk.bgCyan.black.bold(' 🔊 [AUDIO RECIBIDO]: Procesando voz... ')}\n`);
162
167
  this._promptUser();
163
168
  });
164
169
 
@@ -166,7 +171,7 @@ export class HelixRepl {
166
171
  this.client.on('react_step', (step: { iteration?: number; role?: string }) => {
167
172
  const iter = step?.iteration ?? 1;
168
173
  const role = step?.role ? chalk.gray(` (${step.role})`) : '';
169
- process.stdout.write(`\r ${chalk.hex('#FF9F1C')('◆')} ${chalk.gray(`Paso ${iter}${role} — Razonando...`)}`);
174
+ process.stdout.write(`\r ${chalk.hex('#FF9F1C')('◆')} ${chalk.gray(`Paso ${iter}${role} — Razonando...`)}`);
170
175
  });
171
176
 
172
177
  // Agent assistant message
@@ -185,13 +190,13 @@ export class HelixRepl {
185
190
 
186
191
  // Branded header
187
192
  console.log('');
188
- console.log(` ${chalk.hex('#FF8C00').bold('⬡ HELIX')}${modelBadge}`);
193
+ console.log(` ${chalk.hex('#FF8C00').bold('⬡ HELIX')}${modelBadge}`);
189
194
 
190
195
  // Content with subtle left border and clean Markdown formatting
191
196
  const formatted = formatTerminalMarkdown(content, this.plain);
192
197
  const contentLines = formatted.trim().split('\n');
193
198
  for (const line of contentLines) {
194
- console.log(` ${chalk.gray('│')} ${line}`);
199
+ console.log(` ${chalk.gray('│')} ${line}`);
195
200
  }
196
201
  console.log('');
197
202
 
@@ -210,16 +215,39 @@ export class HelixRepl {
210
215
  this.activeTaskId = task.task_id || null;
211
216
  }
212
217
  });
218
+
219
+ this.client.on('browser_frame', (payload: {
220
+ conversation_id?: string;
221
+ mime_type?: string;
222
+ image_base64?: string;
223
+ }) => {
224
+ if (
225
+ !this.browserLiveViewEnabled
226
+ || !payload?.conversation_id?.startsWith('cli_')
227
+ || payload.mime_type !== 'image/jpeg'
228
+ || typeof payload.image_base64 !== 'string'
229
+ ) {
230
+ return;
231
+ }
232
+ try {
233
+ process.stdout.write('\r\x1b[K');
234
+ console.log(`\n${CYAN_BOLD(' HELIX LIVE BROWSER')}`);
235
+ console.log(renderJpegAsAscii(payload.image_base64));
236
+ this._promptUser();
237
+ } catch (error) {
238
+ console.log(RED(` No se pudo representar el cuadro del navegador: ${(error as Error).message}`));
239
+ }
240
+ });
213
241
  }
214
242
 
215
243
  private async _handleConfirmationDialog(req: ConfirmationRequest): Promise<void> {
216
- console.log(`\n${YELLOW_BOLD('⚠️ CONFIRMACIÓN DE ACCIÓN REQUERIDA')}`);
217
- console.log(` Acción: ${chalk.white.bold(req.action || req.tool || 'Acción del sistema')}`);
244
+ console.log(`\n${YELLOW_BOLD('⚠️ CONFIRMACIÓN DE ACCIÓN REQUERIDA')}`);
245
+ console.log(` Acción: ${chalk.white.bold(req.action || req.tool || 'Acción del sistema')}`);
218
246
  if (req.description) {
219
- console.log(` Descripción: ${DIM(req.description)}`);
247
+ console.log(` Descripción: ${DIM(req.description)}`);
220
248
  }
221
249
  if (req.arguments && Object.keys(req.arguments).length > 0) {
222
- console.log(` Parámetros: ${GRAY(JSON.stringify(req.arguments))}`);
250
+ console.log(` Parámetros: ${GRAY(JSON.stringify(req.arguments))}`);
223
251
  }
224
252
 
225
253
  this.rl?.pause();
@@ -227,17 +255,17 @@ export class HelixRepl {
227
255
  const answer = await prompts({
228
256
  type: 'confirm',
229
257
  name: 'confirmed',
230
- message: '¿Permitir al agente ejecutar esta acción?',
258
+ message: '¿Permitir al agente ejecutar esta acción?',
231
259
  initial: true,
232
260
  });
233
261
 
234
262
  const reqId = req.request_id || req.id || '';
235
263
  if (answer && answer.confirmed) {
236
264
  this.client.confirmAction(reqId);
237
- console.log(GREEN(' ✔ Acción confirmada.\n'));
265
+ console.log(GREEN(' ✔ Acción confirmada.\n'));
238
266
  } else {
239
267
  this.client.rejectAction(reqId);
240
- console.log(RED(' ✖ Acción rechazada.\n'));
268
+ console.log(RED(' ✖ Acción rechazada.\n'));
241
269
  }
242
270
  } finally {
243
271
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
@@ -269,7 +297,7 @@ export class HelixRepl {
269
297
  });
270
298
 
271
299
  rl.on('close', () => {
272
- console.log(DIM('\nCerrando sesión de HELIX CLI...'));
300
+ console.log(DIM('\nCerrando sesión de HELIX CLI...'));
273
301
  this.client.close();
274
302
  process.exit(0);
275
303
  });
@@ -283,7 +311,7 @@ export class HelixRepl {
283
311
  this.activeTaskId = null;
284
312
  this._promptUser();
285
313
  } else {
286
- console.log(DIM('\n¡Hasta pronto!'));
314
+ console.log(DIM('\n¡Hasta pronto!'));
287
315
  this.client.close();
288
316
  process.exit(0);
289
317
  }
@@ -294,7 +322,7 @@ export class HelixRepl {
294
322
 
295
323
  private _promptUser(): void {
296
324
  if (!this.rl) return;
297
- const promptPrefix = ` ${chalk.hex('#FF8C00').bold('helix')} ${chalk.gray('❯')} `;
325
+ const promptPrefix = ` ${chalk.hex('#FF8C00').bold('helix')} ${chalk.gray('❯')} `;
298
326
  this.rl.setPrompt(promptPrefix);
299
327
  this.rl.prompt();
300
328
  }
@@ -306,7 +334,7 @@ export class HelixRepl {
306
334
  }
307
335
 
308
336
  if (this.client.isStarting) {
309
- console.log(`\n ${chalk.yellow('⚠️')} ${chalk.yellowBright('El servicio de HELIX se está iniciando en segundo plano. Espera a que termine antes de enviar comandos o reintentar.')}\n`);
337
+ console.log(`\n ${chalk.yellow('⚠️')} ${chalk.yellowBright('El servicio de HELIX se está iniciando en segundo plano. Espera a que termine antes de enviar comandos o reintentar.')}\n`);
310
338
  this._promptUser();
311
339
  return;
312
340
  }
@@ -383,18 +411,48 @@ export class HelixRepl {
383
411
  break;
384
412
  }
385
413
 
386
- case '/browser':
414
+ case '/browser': {
415
+ const browserArgs = args.trim().split(/\s+/);
416
+ if (browserArgs[0]?.toLowerCase() === 'live') {
417
+ const requestedState = browserArgs[1]?.toLowerCase();
418
+ if (requestedState && !['on', 'off'].includes(requestedState)) {
419
+ console.log(YELLOW(' Uso: /browser live [on|off]\n'));
420
+ break;
421
+ }
422
+ this.browserLiveViewEnabled = requestedState
423
+ ? requestedState === 'on'
424
+ : !this.browserLiveViewEnabled;
425
+ console.log(
426
+ this.browserLiveViewEnabled
427
+ ? GREEN(' Vista ASCII del navegador activada.\n')
428
+ : YELLOW(' Vista ASCII del navegador desactivada.\n'),
429
+ );
430
+ break;
431
+ }
387
432
  await handleBrowserCommand(this.client, args);
388
433
  break;
434
+ }
435
+
436
+ case '/subagents':
437
+ await handleSubagentsCommand(this.client, args);
438
+ break;
389
439
 
390
440
  case '/tasks':
391
441
  await this._handleListTasks();
392
442
  break;
393
443
 
444
+ case '/model':
445
+ await handleModelCommand(this.client, args);
446
+ break;
447
+
394
448
  case '/models':
395
449
  await this._handleListModels();
396
450
  break;
397
451
 
452
+ case '/memory':
453
+ await handleMemoryCommand(this.client, args);
454
+ break;
455
+
398
456
  case '/status':
399
457
  await this._handleStatus();
400
458
  break;
@@ -416,7 +474,7 @@ export class HelixRepl {
416
474
  this.client.emergencyStop();
417
475
  this.isProcessingPrompt = false;
418
476
  this.activeTaskId = null;
419
- console.log(GREEN('✔ Todas las tareas han sido detenidas.\n'));
477
+ console.log(GREEN('✔ Todas las tareas han sido detenidas.\n'));
420
478
  break;
421
479
 
422
480
  case '/exit':
@@ -436,7 +494,7 @@ export class HelixRepl {
436
494
 
437
495
  // --- Direct chat message / @skill / #mcp ---
438
496
  if (!this.client.connected) {
439
- console.log(YELLOW('⚠ El agente no está conectado actualmente. Intentando enviar de todos modos...'));
497
+ console.log(YELLOW('⚠ El agente no está conectado actualmente. Intentando enviar de todos modos...'));
440
498
  }
441
499
 
442
500
  // Highlight skills in strong orange (#FF8C00) and mcps in cyan
@@ -448,8 +506,8 @@ export class HelixRepl {
448
506
  this.isProcessingPrompt = true;
449
507
  this.activeTasksCount++;
450
508
 
451
- process.stdout.write(` ${chalk.gray('→')} ${formattedEcho}\n`);
452
- process.stdout.write(` ${chalk.hex('#FF9F1C')('◆')} ${chalk.gray('Procesando...')}`);
509
+ process.stdout.write(` ${chalk.gray('→')} ${formattedEcho}\n`);
510
+ process.stdout.write(` ${chalk.hex('#FF9F1C')('â—†')} ${chalk.gray('Procesando...')}`);
453
511
 
454
512
  const sent = this.client.sendUserText(
455
513
  line,
@@ -459,7 +517,7 @@ export class HelixRepl {
459
517
 
460
518
  if (!sent) {
461
519
  process.stdout.write('\r\x1b[K');
462
- console.log(RED('✖ Error: No se pudo enviar el mensaje (WebSocket desconectado).'));
520
+ console.log(RED('✖ Error: No se pudo enviar el mensaje (WebSocket desconectado).'));
463
521
  this.isProcessingPrompt = false;
464
522
  this.activeTasksCount = Math.max(0, this.activeTasksCount - 1);
465
523
  this._promptUser();
@@ -469,7 +527,7 @@ export class HelixRepl {
469
527
  private async _handleListTasks(): Promise<void> {
470
528
  try {
471
529
  const tasks = await this.client.getTasks();
472
- console.log(`\n${BOLD('📋 Tareas Registradas en HELIX:')}\n`);
530
+ console.log(`\n${BOLD('📋 Tareas Registradas en HELIX:')}\n`);
473
531
  if (!tasks || tasks.length === 0) {
474
532
  console.log(GRAY(' No hay tareas recientes registradas.\n'));
475
533
  return;
@@ -477,10 +535,10 @@ export class HelixRepl {
477
535
 
478
536
  for (const t of tasks.slice(-10)) {
479
537
  let statusBadge = chalk.gray(t.status);
480
- if (t.status === 'COMPLETED') statusBadge = chalk.green('✔ Completada');
481
- if (t.status === 'RUNNING') statusBadge = chalk.yellow('⏳ En ejecución');
482
- if (t.status === 'FAILED') statusBadge = chalk.red('✖ Fallida');
483
- if (t.status === 'CANCELLED') statusBadge = chalk.gray('○ Cancelada');
538
+ if (t.status === 'COMPLETED') statusBadge = chalk.green('✔ Completada');
539
+ if (t.status === 'RUNNING') statusBadge = chalk.yellow('⏳ En ejecución');
540
+ if (t.status === 'FAILED') statusBadge = chalk.red('✖ Fallida');
541
+ if (t.status === 'CANCELLED') statusBadge = chalk.gray('â—‹ Cancelada');
484
542
 
485
543
  console.log(` ${statusBadge} ${DIM(t.task_id)}`);
486
544
  if (t.prompt) console.log(` ${chalk.white(t.prompt)}`);
@@ -494,14 +552,14 @@ export class HelixRepl {
494
552
  private async _handleListModels(): Promise<void> {
495
553
  try {
496
554
  const models = await this.client.getModels();
497
- console.log(`\n${BOLD('🧠 Modelos Disponibles en Proveedores Configurados:')}\n`);
555
+ console.log(`\n${BOLD('🧠 Modelos Disponibles en Proveedores Configurados:')}\n`);
498
556
  if (!models || models.length === 0) {
499
557
  console.log(GRAY(' No hay modelos disponibles. Configura tus API Keys con /config-apikey.\n'));
500
558
  return;
501
559
  }
502
560
 
503
561
  for (const m of models) {
504
- console.log(` ${CYAN('•')} ${chalk.bold(m.id)} ${DIM(`[${m.provider}]`)}`);
562
+ console.log(` ${CYAN('•')} ${chalk.bold(m.id)} ${DIM(`[${m.provider}]`)}`);
505
563
  }
506
564
  console.log('');
507
565
  } catch (err) {
@@ -511,11 +569,11 @@ export class HelixRepl {
511
569
 
512
570
  private async _handleStatus(): Promise<void> {
513
571
  console.log('');
514
- console.log(` ${chalk.hex('#FF8C00').bold('⬡ Estado del Sistema')}`);
515
- console.log(` ${chalk.gray('─'.repeat(40))}`);
516
- console.log(` ${chalk.gray('Agente WS')} ${this.client.connected ? chalk.green('● Conectado') : chalk.red('● Desconectado')} ${chalk.gray(this.client.url)}`);
517
- console.log(` ${chalk.gray('Micrófono')} ${this.voiceEnabled ? chalk.green('● Activo') + chalk.gray(' (hey helix)') : chalk.red('● Inactivo')}`);
518
- console.log(` ${chalk.gray('Conversación')} ${chalk.gray(this.conversationId)} ${chalk.gray(`(${this.conversationHistory.length} msgs)`)}`);
572
+ console.log(` ${chalk.hex('#FF8C00').bold('⬡ Estado del Sistema')}`);
573
+ console.log(` ${chalk.gray('─'.repeat(40))}`);
574
+ console.log(` ${chalk.gray('Agente WS')} ${this.client.connected ? chalk.green('● Conectado') : chalk.red('● Desconectado')} ${chalk.gray(this.client.url)}`);
575
+ console.log(` ${chalk.gray('Micrófono')} ${this.voiceEnabled ? chalk.green('● Activo') + chalk.gray(' (hey helix)') : chalk.red('● Inactivo')}`);
576
+ console.log(` ${chalk.gray('Conversación')} ${chalk.gray(this.conversationId)} ${chalk.gray(`(${this.conversationHistory.length} msgs)`)}`);
519
577
  console.log(` ${chalk.gray('Skills')} ${chalk.hex('#FF8C00').bold(this.autocompleter.getSkills().length.toString())} registradas`);
520
578
  console.log(` ${chalk.gray('MCP Tools')} ${chalk.cyan.bold(this.autocompleter.getMcpTools().length.toString())} disponibles`);
521
579
  console.log('');
package/tests/cli.test.ts CHANGED
@@ -7,6 +7,9 @@ import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { discoverUserSkillNames } from '../src/skills/user-skill-discovery';
10
+ import { renderSubagentPanel } from '../src/commands/subagents';
11
+ import { grayscaleToAscii, renderJpegAsAscii } from '../src/ui/browser-ascii';
12
+ import jpeg from 'jpeg-js';
10
13
 
11
14
  test('Autocompleter suggests slash commands', () => {
12
15
  const completer = new Autocompleter();
@@ -18,6 +21,7 @@ test('Autocompleter suggests slash commands', () => {
18
21
  assert.ok(completions.includes('/mcp'));
19
22
  assert.ok(completions.includes('/create-mcp'));
20
23
  assert.ok(completions.includes('/browser'));
24
+ assert.ok(completions.includes('/subagents'));
21
25
  assert.ok(completions.includes('/config-apikey'));
22
26
  assert.ok(completions.includes('/show-apikey'));
23
27
  assert.ok(completions.includes('/microphone'));
@@ -28,6 +32,43 @@ test('Autocompleter suggests slash commands', () => {
28
32
  assert.ok(completions.includes('/clear'));
29
33
  });
30
34
 
35
+ test('Subagent panel renders ASCII rows with current status and action', () => {
36
+ const panel = renderSubagentPanel(
37
+ [{ profile_id: 'flowbot', name: 'Flowbot', role: 'Browser automation' }],
38
+ [{
39
+ profile_id: 'flowbot',
40
+ status: 'running',
41
+ progress: 50,
42
+ current_action: 'Opening browser',
43
+ }],
44
+ );
45
+
46
+ assert.ok(panel.includes('+'));
47
+ assert.ok(panel.includes('flowbot'));
48
+ assert.ok(panel.includes('Flowbot'));
49
+ assert.ok(panel.includes('RUNNING'));
50
+ assert.ok(panel.includes('50%'));
51
+ assert.ok(panel.includes('Opening browser'));
52
+ });
53
+
54
+ test('Browser image renderer maps grayscale to a bounded ASCII frame', () => {
55
+ assert.equal(
56
+ grayscaleToAscii([[0, 255], [0, 255], [128, 64], [128, 64]], 2),
57
+ '@ \n=#',
58
+ );
59
+ });
60
+
61
+ test('Browser image renderer decodes JPEG frames and rejects invalid payloads', () => {
62
+ const image = jpeg.encode({
63
+ data: Buffer.from([255, 255, 255, 255]),
64
+ width: 1,
65
+ height: 1,
66
+ }, 80);
67
+
68
+ assert.equal(renderJpegAsAscii(image.data.toString('base64'), 1), ' ');
69
+ assert.throws(() => renderJpegAsAscii('not-a-jpeg'), /valid, bounded Base64 JPEG/);
70
+ });
71
+
31
72
  test('Autocompleter filters slash commands by prefix', () => {
32
73
  const completer = new Autocompleter();
33
74
 
package/tsconfig.json CHANGED
@@ -1,4 +1,4 @@
1
- {
1
+ {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
4
  "module": "commonjs",
@@ -9,11 +9,11 @@
9
9
  "esModuleInterop": true,
10
10
  "skipLibCheck": true,
11
11
  "forceConsistentCasingInFileNames": true,
12
- // @ts-ignore - node is deprecated but required for CommonJS compatibility with current dependencies
13
12
  "moduleResolution": "node",
14
13
  "resolveJsonModule": true,
15
- "declaration": true
14
+ "declaration": true,
15
+ "types": ["node", "ws"]
16
16
  },
17
17
  "include": ["src/**/*"],
18
18
  "exclude": ["node_modules", "dist"]
19
- }
19
+ }