primocode 10.4.2 → 10.4.3

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 CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v10.4.2
1
+ # PrimoCode v10.4.3
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
9
9
  está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
10
10
 
11
11
  ```
12
- ▐▛███▜▌ Primo Code v10.4.2
12
+ ▐▛███▜▌ Primo Code v10.4.3
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/bin/primocode.js CHANGED
@@ -305,6 +305,8 @@ function sliderEffort(atual) {
305
305
  let anima = null;
306
306
 
307
307
  const pintar = (primeira) => {
308
+ // Na tela cheia o slider desenha no painel dela, acima da caixa.
309
+ if (rl && rl.cheia) return rl.painel(effort.desenharEffort(idx, fase));
308
310
  if (!primeira) process.stdout.write(`\x1b[${ALTURA}A\r`);
309
311
  process.stdout.write(effort.desenharEffort(idx, fase).split('\n').map((l) => '\x1b[2K' + l).join('\n') + '\n');
310
312
  };
@@ -466,7 +468,7 @@ function barraDeStatus() {
466
468
  : uso.pct >= 70 ? c.warn(`${uso.pct}% ctx`)
467
469
  : c.dim(`${uso.pct}% ctx`));
468
470
  }
469
- direita.push(state.trabalhando
471
+ direita.push(state.trabalhando && rl && rl.ocupado
470
472
  ? c.warn('trabalhando') + c.dim(' · ') + c.white('esc esc') + c.dim(' para')
471
473
  : c.white('tab') + c.dim(' modos ') + c.white('ctrl+p') + c.dim(' comandos'));
472
474
 
@@ -920,6 +922,7 @@ function abrirMarketplace() {
920
922
  const texto = marketplace.montar(estado, {
921
923
  colunas: process.stdout.columns || 80, altura: altura(),
922
924
  });
925
+ if (rl && rl.cheia) return rl.painel(texto);
923
926
  const linhas = texto.split('\n');
924
927
  // Redesenha POR CIMA do quadro anterior em vez de rolar a tela:
925
928
  // sem isso, cada seta empurraria a tela inteira para cima e o
@@ -2760,6 +2763,8 @@ async function main() {
2760
2763
  vazio: ui.convite(),
2761
2764
  telaCheia,
2762
2765
  lateral: barraLateral,
2766
+ logoAnimado: (quadro, trabalhando) => ui.logoAnimado(state.config.mascot, quadro, trabalhando),
2767
+ agora: () => (ui.agora.texto && Date.now() - ui.agora.desde < 10 * 60 * 1000 ? ui.agora.texto : ''),
2763
2768
  versao: VERSION,
2764
2769
  logo: ui.logoComMascote(state.config.mascot),
2765
2770
  pasta: () => {
@@ -2852,6 +2857,9 @@ async function main() {
2852
2857
  const text = queue.shift().trim();
2853
2858
  if (!text) continue;
2854
2859
  ui.titulo(`⠋ PrimoCode — ${text}`);
2860
+ // Comando (/effort, /help) não é trabalho do agente: a caixa e a
2861
+ // barra da direita só dizem "trabalhando" quando ele trabalha.
2862
+ if (rl.trabalhando) rl.trabalhando(!isSlash(text));
2855
2863
  try {
2856
2864
  if (ehBash(text)) await rodarBash(text);
2857
2865
  else if (isSlash(text)) await handleSlash(text);
package/lib/entrada.js CHANGED
@@ -41,6 +41,7 @@ const readline = require('readline');
41
41
  const { EventEmitter } = require('events');
42
42
  const { c, visibleWidth, truncate } = require('./ui.js');
43
43
  const tela = require('./tela.js');
44
+ const stripAnsiLocal = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, '');
44
45
 
45
46
  // ── O desenho ────────────────────────────────────────────────────────────
46
47
 
@@ -323,7 +324,6 @@ class Entrada extends EventEmitter {
323
324
  e quem põe na tela é o próximo quadro. Suspensa (o painel do
324
325
  /effort desenhando por cima), passa direto. */
325
326
  if (eu.cheia) {
326
- if (eu.suspenso) return eu._stdoutOriginal.call(this, texto, ...resto);
327
327
  tela.acrescentar(eu.conversa, texto);
328
328
  eu._agendar();
329
329
  const fim = resto.find((r) => typeof r === 'function');
@@ -388,13 +388,23 @@ class Entrada extends EventEmitter {
388
388
  /* Enquanto outro desenho é dono da tela — o painel do /effort, uma tela
389
389
  do marketplace — a caixa sai da frente e larga o teclado. Sem isso, os
390
390
  dois desenhariam no mesmo lugar e as setas moveriam os dois. */
391
- suspender() { this.suspenso = true; if (this.cheia) this._sairDaTela(); else this._apagar(); }
391
+ /* Na tela cheia, suspender só LARGA O TECLADO: a tela continua no ar, e
392
+ quem suspendeu desenha pelo `painel()`. Sair da tela alternativa aqui
393
+ (o jeito antigo) jogava o /effort por cima do terminal velho, e a
394
+ volta deixava tudo bagunçado. */
395
+ suspender() { this.suspenso = true; if (this.cheia) this._agendar(); else this._apagar(); }
392
396
  retomar() {
393
397
  this.suspenso = false;
394
- if (this.cheia && this.ativo && !this.fechado) { this._entrarNaTela(); return this._pintar(); }
398
+ if (this.cheia && this.ativo && !this.fechado) { this.painelTexto = null; return this._pintar(); }
395
399
  if (this.ativo && !this.fechado) this._desenhar();
396
400
  }
397
401
 
402
+ /** Um comando com tela própria desenha aqui (na tela cheia). null fecha. */
403
+ painel(texto) {
404
+ this.painelTexto = texto == null ? null : String(texto).replace(/\n+$/, '');
405
+ this._pintar();
406
+ }
407
+
398
408
  // ── a tela cheia ─────────────────────────────────────────────────────
399
409
 
400
410
  /* A tela alternativa (a do vim): o que estava no terminal fica guardado e
@@ -412,7 +422,7 @@ class Entrada extends EventEmitter {
412
422
  this._colunasReais = process.stdout.columns || 80;
413
423
  Object.defineProperty(process.stdout, 'columns', {
414
424
  configurable: true,
415
- get() { return eu._naTela && !eu.suspenso ? tela.larguraDaConversa(eu._colunasReais) : eu._colunasReais; },
425
+ get() { return eu._naTela ? tela.larguraDaConversa(eu._colunasReais) : eu._colunasReais; },
416
426
  set(v) { eu._colunasReais = v; }, // o Node escreve aqui quando a janela muda
417
427
  });
418
428
  }
@@ -440,14 +450,36 @@ class Entrada extends EventEmitter {
440
450
  /** O estado do quadro, para a tela cheia (função pura em lib/tela.js). */
441
451
  _quadroCheio() {
442
452
  const conversa = this.conversa.some((l) => /\S/.test(l));
453
+ const trabalhando = !!(this.ocupado || this.spinnerTexto);
454
+ const quadro = Date.now() / 140;
455
+ /* O topo da barra da direita: o logo do PrimoCode (que anima enquanto
456
+ ele trabalha) e o ESTADO — trabalhando, há quanto tempo, no quê. */
457
+ const topo = this.op.logoAnimado ? this.op.logoAnimado(quadro, trabalhando) : [];
458
+ topo.push('');
459
+ if (trabalhando) {
460
+ const giro = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'][Math.floor(quadro) % 10];
461
+ topo.push(c.warn(giro + ' Trabalhando') + (this._desde ? c.dim(` · ${Math.floor((Date.now() - this._desde) / 1000)}s`) : ''));
462
+ const agora = this.op.agora ? this.op.agora() : '';
463
+ if (agora) topo.push(c.muted(agora));
464
+ if (this.spinnerTexto) topo.push(c.dim(stripAnsiLocal(this.spinnerTexto).replace(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s*/, '')));
465
+ } else {
466
+ topo.push(c.brand('● ') + c.muted('Pronto') + c.dim(' · esperando você'));
467
+ }
468
+ if (trabalhando && !this._animando) {
469
+ // Enquanto trabalha, a barra se redesenha sozinha (o logo anima
470
+ // e o relógio anda), mesmo que nada novo chegue na conversa.
471
+ this._animando = setInterval(() => this._pintar(), 140);
472
+ if (this._animando.unref) this._animando.unref();
473
+ } else if (!trabalhando && this._animando) { clearInterval(this._animando); this._animando = null; }
443
474
  return tela.quadro({
444
475
  colunas: this._colunasReais || 80,
445
476
  linhas: process.stdout.rows || 24,
446
477
  conversa: this.conversa,
447
478
  rolagem: this.rolagem,
448
- home: !this.mandou && !conversa,
479
+ home: !this.mandou && !conversa && !this.painelTexto,
449
480
  caixa: (colunas, centro) => montar({ ...this._estado(), colunas, centro, menuAcima: true }),
450
- lateral: { titulo: this.titulo, ...(this.op.lateral ? this.op.lateral() : {}) },
481
+ lateral: { titulo: this.titulo, topo, ...(this.op.lateral ? this.op.lateral() : {}) },
482
+ painel: this.painelTexto,
451
483
  versao: this.op.versao || '',
452
484
  logo: this.op.logo,
453
485
  pasta: this.op.pasta ? this.op.pasta() : '',
@@ -455,7 +487,7 @@ class Entrada extends EventEmitter {
455
487
  }
456
488
 
457
489
  _pintar() {
458
- if (!this.cheia || this.suspenso || this.fechado || !this._naTela) return;
490
+ if (!this.cheia || this.fechado || !this._naTela) return;
459
491
  const q = this._quadroCheio();
460
492
  this.rolagem = Math.min(this.rolagem, q.rolagemMax);
461
493
  let out = '\x1b[?25l';
@@ -477,6 +509,7 @@ class Entrada extends EventEmitter {
477
509
  trabalhando(sim) {
478
510
  if (this.ocupado === !!sim) return;
479
511
  this.ocupado = !!sim;
512
+ this._desde = sim ? Date.now() : 0;
480
513
  if (this.desenhado) this._redesenhar();
481
514
  }
482
515
 
package/lib/tela.js CHANGED
@@ -116,6 +116,8 @@ function caber(s, w, fundo) {
116
116
  function lateral(lat, alto) {
117
117
  const w = LATERAL_W - 4; // 2 de margem de cada lado
118
118
  const corpo = [''];
119
+ // O topo: o logo (animado enquanto trabalha) e o estado do Primo agora.
120
+ if (lat.topo && lat.topo.length) corpo.push(...lat.topo, '');
119
121
  for (const l of quebrarComCor(c.bold(c.white(lat.titulo || 'Nova sessão')), w)) corpo.push(l);
120
122
  for (const s of (lat.secoes || [])) {
121
123
  corpo.push('');
@@ -179,6 +181,10 @@ function quadro(e) {
179
181
  };
180
182
  }
181
183
 
184
+ /* O PAINEL de um comando que desenha a própria tela (o slider do
185
+ /effort, o marketplace): fica no pé da conversa, logo acima da caixa,
186
+ por cima do que estiver ali — a tela cheia nunca sai do ar. */
187
+ const painel = (e.painel ? String(e.painel).split('\n') : []);
182
188
  const comLateral = W >= LATERAL_MIN;
183
189
  const mainW = comLateral ? W - LATERAL_W : W;
184
190
  /* Uma coluna de margem dos dois lados, para a conversa e para a caixa: o
@@ -207,6 +213,11 @@ function quadro(e) {
207
213
  const conversa = [];
208
214
  for (let i = janela.length; i < altoConversa; i++) conversa.push('');
209
215
  for (const l of janela) conversa.push(l);
216
+ if (painel.length) {
217
+ const inicio = Math.max(0, conversa.length - painel.length - 1);
218
+ conversa.splice(inicio, conversa.length - inicio, '', ...painel.map((l) => ' ' + l));
219
+ conversa.length = altoConversa;
220
+ }
210
221
  if (rolagem > 0) {
211
222
  conversa[0] = ' ' + c.dim(`↑ ${rolagem} linha${rolagem > 1 ? 's' : ''} abaixo · PgDn ou a roda do mouse para voltar`);
212
223
  }
package/lib/ui.js CHANGED
@@ -358,12 +358,75 @@ function logo() {
358
358
  }
359
359
  const LOGO_W = visibleWidth(logo()[0]);
360
360
 
361
- /** O logo com o mascote do PrimoCode na frente — o desenho de 3 linhas
362
- * alinhado às 3 linhas do corpo das letras (a 4ª é só a perna do p). */
361
+ /* ── O mascote GRANDE ─────────────────────────────────────────────────
362
+ O desenho de sempre tem 3 linhas — pequeno ao lado do logo de 4. Para a
363
+ home ele cresce 2×: cada quarto de bloco (▘ ▝ ▖ ▗) vira um bloco cheio, e
364
+ cada bloco cheio vira 2×2. Ficam 6 linhas × 18 colunas, com a cor de cada
365
+ pedaço preservada — o mesmo desenho, não um desenho novo. */
366
+ const QUARTOS = {
367
+ ' ': [0, 0, 0, 0], '█': [1, 1, 1, 1], '▀': [1, 1, 0, 0], '▄': [0, 0, 1, 1],
368
+ '▌': [1, 0, 1, 0], '▐': [0, 1, 0, 1], '▘': [1, 0, 0, 0], '▝': [0, 1, 0, 0],
369
+ '▖': [0, 0, 1, 0], '▗': [0, 0, 0, 1], '▛': [1, 1, 1, 0], '▜': [1, 1, 0, 1],
370
+ '▙': [1, 0, 1, 1], '▟': [0, 1, 1, 1], '▚': [1, 0, 0, 1], '▞': [0, 1, 1, 0],
371
+ };
372
+ function mascoteGrande(style) {
373
+ const saida = [];
374
+ for (const linha of mascot(style)) {
375
+ // [caractere, cor] de cada coluna da linha colorida
376
+ const cels = [];
377
+ let cor = '';
378
+ for (const m of linha.matchAll(/(\x1b\[[0-9;]*m)|([^\x1b])/gu)) {
379
+ if (m[1]) { if (m[1] !== PALETTE.reset) cor = m[1]; continue; }
380
+ cels.push([m[2], cor]);
381
+ }
382
+ const cima = [], baixo = [];
383
+ for (const [ch, cr] of cels) {
384
+ const q = QUARTOS[ch] || (ch === ' ' ? QUARTOS[' '] : QUARTOS['█']);
385
+ const bloco = (b) => (b ? `${cr}█${PALETTE.reset}` : ' ');
386
+ cima.push(bloco(q[0]) + bloco(q[1]));
387
+ baixo.push(bloco(q[2]) + bloco(q[3]));
388
+ }
389
+ saida.push(cima.join(''), baixo.join(''));
390
+ }
391
+ while (saida.length && !saida[saida.length - 1].trim()) saida.pop(); // a linha vazia de baixo
392
+ return saida;
393
+ }
394
+
395
+ /** O logo da home: o mascote grande e, centrado na altura dele, o letreiro. */
363
396
  function logoComMascote(style) {
364
- const m = mascot(style);
397
+ const m = mascoteGrande(style);
365
398
  const l = logo();
366
- return l.map((linha, i) => (m[i] || ' '.repeat(ART_W)) + ' ' + linha);
399
+ const desce = Math.floor((m.length - l.length) / 2);
400
+ const linhas = m.map((linha, i) => linha + ' ' + (l[i - desce] || ''));
401
+ // Todas com a MESMA largura: a home centraliza linha por linha, e uma
402
+ // linha mais curta (a dos pés) sairia deslocada do resto do desenho.
403
+ const larg = Math.max(...linhas.map(visibleWidth));
404
+ return linhas.map((x) => x + ' '.repeat(larg - visibleWidth(x)));
405
+ }
406
+
407
+ /* ── O logo animado da barra da direita ──────────────────────────────────
408
+ Enquanto o Primo trabalha, o mascote pulsa (a cor viva respira entre três
409
+ tons de ciano), os pés andam, e um brilho corre pelo nome "PrimoCode".
410
+ Parado, fica quieto: animação sem trabalho é ruído. `quadro` é um número
411
+ que cresce com o tempo (o chamador passa Date.now()/120). */
412
+ const PULSO = ['\x1b[38;5;45m', '\x1b[38;5;51m', '\x1b[38;5;87m', '\x1b[38;5;51m'];
413
+ function logoAnimado(style, quadro, trabalhando) {
414
+ const f = Math.floor(quadro || 0);
415
+ const m = MASCOTS[style] || MASCOTS.primo;
416
+ // Os pés andam: a linha dos pés troca de passo a cada quadro.
417
+ const linhas = m.linhas.map((l) => (trabalhando && f % 2 && l === PES ? ' ▝▘ ▘▝ ' : l));
418
+ let art = linhas.map((l, i) => shade(l, m, m.mascara && m.mascara[i]));
419
+ if (trabalhando) art = art.map((l) => l.split(PALETTE.brand).join(PULSO[f % PULSO.length]));
420
+ const nome = 'PrimoCode';
421
+ const brilho = f % (nome.length + 8);
422
+ let texto = '';
423
+ [...nome].forEach((ch, i) => {
424
+ const base = i < 5 ? '\x1b[38;5;255m' : '\x1b[38;5;45m';
425
+ const aceso = trabalhando && Math.abs(i - brilho) <= 0;
426
+ const perto = trabalhando && Math.abs(i - brilho) === 1;
427
+ texto += (aceso ? '\x1b[1m\x1b[38;5;231m' : perto ? '\x1b[1m\x1b[38;5;159m' : '\x1b[1m' + base) + ch + PALETTE.reset;
428
+ });
429
+ return [art[0], art[1] + ' ' + texto, art[2]];
367
430
  }
368
431
 
369
432
  /** Centraliza uma linha (com cor) em `cols` colunas. */
@@ -467,6 +530,8 @@ function titulo(texto) {
467
530
  try { escrever(`\x1b]0;${limpo}\x07`); } catch { /* terminal fechado */ }
468
531
  }
469
532
 
533
+ const agora = { texto: '', desde: 0 };
534
+
470
535
  // O rótulo de cada tool na tela — o que a pessoa lê no lugar do nome interno.
471
536
  const ROTULOS = {
472
537
  read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
@@ -508,6 +573,9 @@ function actionHeader(name, args) {
508
573
  // virava "[object Object]" na tela.
509
574
  if (argStr && typeof argStr === 'object') argStr = JSON.stringify(argStr).slice(0, 60);
510
575
  argStr = String(argStr || '').replace(/\s+/g, ' ').trim();
576
+ // O que ele está fazendo AGORA — a barra da direita da tela cheia mostra.
577
+ agora.texto = label + (argStr ? ' · ' + argStr.slice(0, 60) : '');
578
+ agora.desde = Date.now();
511
579
  // Uma tool sem argumento nenhum caía no JSON.stringify e desenhava "{}"
512
580
  // ao lado do nome — ruído que não diz nada.
513
581
  if (argStr === '{}' || argStr === 'null' || argStr === 'undefined') argStr = '';
@@ -641,4 +709,4 @@ function friendlyError(raw) {
641
709
 
642
710
 
643
711
  module.exports = {
644
- titulo, c, logo, logoComMascote, letreiro, convite, EXEMPLOS, mascot, MASCOTES, resolverMascote, mascoteInfo, galeriaMascotes, reacao, welcomeBox, Spinner, PENSANDO, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
712
+ titulo, agora, c, logo, logoComMascote, mascoteGrande, logoAnimado, letreiro, convite, EXEMPLOS, mascot, MASCOTES, resolverMascote, mascoteInfo, galeriaMascotes, reacao, welcomeBox, Spinner, PENSANDO, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.4.2",
3
+ "version": "10.4.3",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {