primocode 11.2.0 → 11.3.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.
@@ -162,11 +162,11 @@ function quebrar(texto, w) {
162
162
  * @returns {{linhas: string[], cursorLinha: number, cursorColuna: number}}
163
163
  */
164
164
  function montar(e) {
165
- const colunas = Math.max(24, Math.min(e.colunas || 80, 200));
165
+ const colunas = Math.max(24, Math.min(e.colunas || 80, 500));
166
166
  /* Na ABERTURA (nada mandado ainda) a caixa é estreita e centrada embaixo
167
167
  do logo, como a home do OpenCode; depois do primeiro pedido ela ocupa a
168
168
  largura toda. `recuo` é a margem da esquerda, que entra em toda linha. */
169
- const largura = e.centro ? Math.min(colunas - 1, 80) : colunas - 1; // a última coluna
169
+ const largura = e.centro ? Math.min(colunas - 1, Math.max(80, Math.min(Math.round(colunas * 0.6), 160))) : colunas - 1; // a última coluna
170
170
  // fica livre: escrever nela faz o terminal quebrar a linha
171
171
  const recuo = e.centro ? Math.max(0, Math.floor((colunas - largura) / 2)) : 0;
172
172
  const w = Math.max(8, largura - PREFIXO);
@@ -237,15 +237,20 @@ function montar(e) {
237
237
  // ── a linha de baixo: pasta à esquerda, atalhos à direita ────────────
238
238
  if (st.linhas) {
239
239
  for (const linha of st.linhas) out.push(' ' + truncate(linha, largura - 2));
240
- } else {
240
+ } else if (!e.centro) {
241
241
  /* Uma linha vazia entre o painel e o rodapé. Colado, o rodapé parecia
242
242
  SOBREPOSTO ao painel em terminais de entrelinha apertada (o Orca):
243
243
  o fundo do painel encostava nas letras da pasta. */
244
244
  out.push('');
245
245
  const esq = st.esquerda || '';
246
- const dir = st.direita || '';
246
+ let dir = st.direita || '';
247
+ /* Sem espaço para tudo, ficam os ATALHOS (é o que a pessoa não sabe que existe) e a pasta
248
+ encolhe; antes disto a linha perdia os atalhos e ficava só a pasta. */
249
+ if (largura - 2 - visibleWidth(esq) - visibleWidth(dir) < 2 && st.atalhos) dir = st.atalhos;
247
250
  const sobra = largura - 2 - visibleWidth(esq) - visibleWidth(dir);
248
- out.push(' ' + (sobra >= 2 ? esq + ' '.repeat(sobra) + dir : truncate(esq, largura - 2)));
251
+ out.push(' ' + (sobra >= 2 ? esq + ' '.repeat(sobra) + dir
252
+ : visibleWidth(dir) + 14 <= largura - 2 ? truncate(esq, largura - 2 - visibleWidth(dir) - 2) + ' ' + dir
253
+ : truncate(esq, largura - 2)));
249
254
  }
250
255
 
251
256
  // ── o menu dos comandos ──────────────────────────────────────────────
@@ -296,7 +301,7 @@ function montar(e) {
296
301
 
297
302
  /** O pedido que foi mandado, como fica no histórico da tela: um painel. */
298
303
  function pedidoNaTela(texto, colunas, guardados = []) {
299
- const largura = Math.max(24, Math.min(colunas || 80, 200)) - 1;
304
+ const largura = Math.max(24, Math.min(colunas || 80, 500)) - 1;
300
305
  const barra = `${COR.barra}┃${COR.reset}`;
301
306
  const linhas = quebrar(texto, Math.max(8, largura - PREFIXO))
302
307
  .map((l) => painel(barra + ' ' + pintarEtiquetas(c.white(l.texto)), largura));
@@ -338,6 +343,12 @@ class Entrada extends EventEmitter {
338
343
  this.cheia = !!(op.telaCheia && this.tty);
339
344
  this.conversa = [];
340
345
  this.rolagem = 0;
346
+ // Diálogo aberto (paleta, modelo, agente, sessões, tema), o aviso do canto e a barra da direita.
347
+ this.dialogo = null;
348
+ this.toastAtual = null;
349
+ this.lateralOculta = false;
350
+ this._lider = false;
351
+ this._modoCor = tela.modoDeCor();
341
352
  }
342
353
 
343
354
  // ── ciclo de vida ────────────────────────────────────────────────────
@@ -360,7 +371,7 @@ class Entrada extends EventEmitter {
360
371
  this._aoTeclar = (ch, key) => this._tecla(ch, key);
361
372
  process.stdin.on('keypress', this._aoTeclar);
362
373
 
363
- this._aoRedimensionar = () => { if (this.cheia) { this._quadroAnterior = null; this._pintar(); } else if (this.desenhado) this._redesenhar(); };
374
+ this._aoRedimensionar = () => { if (this.cheia) { if (this._naTela) this.escreverCru('\x1b[2J'); this._quadroAnterior = null; this._pintar(); } else if (this.desenhado) this._redesenhar(); };
364
375
  process.stdout.on('resize', this._aoRedimensionar);
365
376
 
366
377
  /* Todo `console.log` do programa passa a apagar a caixa antes de
@@ -384,6 +395,7 @@ class Entrada extends EventEmitter {
384
395
  return true;
385
396
  }
386
397
  eu._apagar();
398
+ if (eu._modoCor === 'none') texto = tela.adaptarCor(texto, 'none');
387
399
  const r = eu._stdoutOriginal.call(this, texto, ...resto);
388
400
  /* ── O TEXTO QUE CHEGA EM PEDAÇOS ────────────────────────────
389
401
  A resposta do chat sai em fluxo: dezenas de `write` pequenos,
@@ -495,7 +507,7 @@ class Entrada extends EventEmitter {
495
507
  this._colunasReais = process.stdout.columns || 80;
496
508
  Object.defineProperty(process.stdout, 'columns', {
497
509
  configurable: true,
498
- get() { return eu._naTela ? tela.larguraDaConversa(eu._colunasReais) : eu._colunasReais; },
510
+ get() { return eu._naTela ? tela.larguraDaConversa(eu._colunasReais, eu.lateralOculta) : eu._colunasReais; },
499
511
  set(v) { eu._colunasReais = v; }, // o Node escreve aqui quando a janela muda
500
512
  });
501
513
  }
@@ -530,16 +542,15 @@ class Entrada extends EventEmitter {
530
542
  const quadro = Date.now() / 140;
531
543
  /* O topo da barra da direita: o logo do PrimoCode (que anima enquanto
532
544
  ele trabalha) e o ESTADO — trabalhando, há quanto tempo, no quê. */
533
- const topo = this.op.logoAnimado ? this.op.logoAnimado(quadro, trabalhando) : [];
534
- topo.push('');
545
+ const pe = this.op.logoAnimado ? this.op.logoAnimado(quadro, trabalhando) : [];
535
546
  if (trabalhando) {
536
547
  const giro = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'][Math.floor(quadro) % 10];
537
- topo.push(c.warn(giro + ' Trabalhando') + (this._desde ? c.dim(` · ${Math.floor((Date.now() - this._desde) / 1000)}s`) : ''));
548
+ pe.push(c.warn(giro + ' Trabalhando') + (this._desde ? c.dim(` · ${Math.floor((Date.now() - this._desde) / 1000)}s`) : ''));
538
549
  const agora = this.op.agora ? this.op.agora() : '';
539
- if (agora) topo.push(c.muted(agora));
540
- if (this.spinnerTexto) topo.push(c.dim(stripAnsiLocal(this.spinnerTexto).replace(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s*/, '')));
550
+ const detalhe = agora || (this.spinnerTexto ? stripAnsiLocal(this.spinnerTexto).replace(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s*/, '') : '');
551
+ if (detalhe) pe.push(c.dim(detalhe));
541
552
  } else {
542
- topo.push(c.brand('● ') + c.muted('Pronto') + c.dim(' · esperando você'));
553
+ pe.push(c.brand('● ') + c.muted('Pronto') + c.dim(' · esperando você'));
543
554
  }
544
555
  if (trabalhando && !this._animando) {
545
556
  // Enquanto trabalha, a barra se redesenha sozinha (o logo anima
@@ -554,10 +565,16 @@ class Entrada extends EventEmitter {
554
565
  rolagem: this.rolagem,
555
566
  home: !this.mandou && !conversa && !this.painelTexto,
556
567
  caixa: (colunas, centro) => montar({ ...this._estado(), colunas, centro, menuAcima: true }),
557
- lateral: { titulo: this.titulo, topo, ...(this.op.lateral ? this.op.lateral() : {}) },
568
+ lateral: { titulo: this.titulo, pe, ...(this.op.lateral ? this.op.lateral() : {}) },
569
+ lateralOculta: this.lateralOculta,
570
+ dialogo: this._dialogoParaTela(),
571
+ toast: this.toastAtual,
558
572
  painel: this.painelTexto,
559
573
  versao: this.op.versao || '',
574
+ plano: this.op.plano ? this.op.plano() : '',
575
+ comandosHome: this.op.comandosHome,
560
576
  logo: this.op.logo,
577
+ logoSimples: this.op.logoSimples,
561
578
  pasta: this.op.pasta ? this.op.pasta() : '',
562
579
  });
563
580
  }
@@ -575,7 +592,7 @@ class Entrada extends EventEmitter {
575
592
  let corpo = '';
576
593
  q.linhas.forEach((l, i) => {
577
594
  if (mesmoTamanho && antes.linhas[i] === l) return;
578
- corpo += `\x1b[${i + 1};1H${l}\x1b[0m\x1b[K`;
595
+ corpo += `\x1b[${i + 1};1H${tela.adaptarCor(l, this._modoCor)}\x1b[0m\x1b[K`;
579
596
  });
580
597
  const mesmoCursor = antes && antes.cursor.linha === q.cursor.linha && antes.cursor.coluna === q.cursor.coluna;
581
598
  this._quadroAnterior = { linhas: q.linhas, cursor: q.cursor };
@@ -816,6 +833,9 @@ class Entrada extends EventEmitter {
816
833
  return;
817
834
  }
818
835
 
836
+ if (this.dialogo) return this._teclaDialogo(ch, key);
837
+ if (this._lider) { this._lider = false; if (key && !key.ctrl && key.name !== 'escape') return this._atalhoDoLider(key); }
838
+
819
839
  if (!key) { if (ch) this.write(ch); return; }
820
840
 
821
841
  // Esc Esc e o resto das teclas de fora: quem escuta decide. A caixa
@@ -935,16 +955,121 @@ class Entrada extends EventEmitter {
935
955
  if (this.cheia) { this.conversa = []; this.rolagem = 0; return this._pintar(); }
936
956
  this._apagar(); this.escreverCru('\x1b[2J\x1b[H'); return this._desenhar();
937
957
  case 'j': return this._novaLinha();
938
- // Ctrl+P abre os comandos, como no OpenCode: o mesmo menu do "/".
958
+ // Ctrl+P abre a paleta de comandos, como no OpenCode (a lista com busca no meio da tela).
939
959
  case 'p':
960
+ if (this.cheia && this.op.paleta) return this.abrirDialogo({ titulo: 'Comandos', itens: this.op.paleta(), aoEscolher: (it) => this._rodarItem(it) });
940
961
  if (this.line && !this.line.startsWith('/')) return;
941
962
  if (!this.line) { this.line = '/'; this.cursor = 1; }
942
963
  this._filtrarMenu();
943
964
  return this._redesenhar();
965
+ // Ctrl+X é o "líder" do OpenCode: a tecla seguinte escolhe (n nova, m modelo, a agente, l sessões, t tema, b barra).
966
+ case 'x':
967
+ if (!this.cheia) return;
968
+ this._lider = true;
969
+ clearTimeout(this._liderTempo);
970
+ this._liderTempo = setTimeout(() => { this._lider = false; }, 2500);
971
+ if (this._liderTempo.unref) this._liderTempo.unref();
972
+ this.toast('ctrl+x n nova · m modo · a agentes · l sessões · t tema · b barra', 'info', 2400);
973
+ return;
944
974
  default: return;
945
975
  }
946
976
  }
947
977
 
978
+ // ── diálogos, paleta e avisos (o `dialog-*` e o toast do OpenCode) ────
979
+
980
+ /**
981
+ * Abre uma lista com busca no meio da tela.
982
+ * @param {{titulo:string, itens:Array<{nome,desc,tecla,secao}>|(()=>Array), aoEscolher:(item:object)=>void}} def
983
+ */
984
+ abrirDialogo(def) {
985
+ this.menu = null;
986
+ this.dialogo = { ...def, filtro: '', sel: 0 };
987
+ this._pintar();
988
+ }
989
+
990
+ /** Volta à home: conversa, título e tarefas zeradas (o /clear e o ctrl+x n). */
991
+ novaConversa() {
992
+ this.conversa = []; this.rolagem = 0; this.titulo = ''; this.mandou = false;
993
+ this._quadroAnterior = null;
994
+ if (this.cheia) this._pintar();
995
+ }
996
+
997
+ fecharDialogo() { this.dialogo = null; this._quadroAnterior = null; this._pintar(); }
998
+
999
+ _itensDoDialogo() {
1000
+ const d = this.dialogo;
1001
+ const todos = typeof d.itens === 'function' ? d.itens() : (d.itens || []);
1002
+ const sem = (t) => String(t || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
1003
+ const q = sem(d.filtro).trim();
1004
+ if (!q) return todos;
1005
+ return todos.filter((it) => q.split(/\s+/).every((p) => sem(`${it.nome} ${stripAnsiLocal(it.desc || '')} ${it.secao || ''}`).includes(p)));
1006
+ }
1007
+
1008
+ _dialogoParaTela() {
1009
+ if (!this.dialogo) return null;
1010
+ const itens = this._itensDoDialogo();
1011
+ this.dialogo.sel = Math.max(0, Math.min(this.dialogo.sel, itens.length - 1));
1012
+ return { titulo: this.dialogo.titulo, filtro: this.dialogo.filtro, itens, sel: this.dialogo.sel, vazio: this.dialogo.vazio };
1013
+ }
1014
+
1015
+ _teclaDialogo(ch, key) {
1016
+ const d = this.dialogo;
1017
+ const seq = (key && key.sequence) || ch || '';
1018
+ const n = this._itensDoDialogo().length;
1019
+ const mover = (p) => { d.sel = Math.max(0, Math.min(n - 1, d.sel + p)); this._pintar(); };
1020
+ if (key && key.name === 'escape') return this.fecharDialogo();
1021
+ if (key && key.ctrl && key.name === 'c') return this.fecharDialogo();
1022
+ if (key && (key.name === 'up' || (key.ctrl && key.name === 'p'))) return mover(-1);
1023
+ if (key && (key.name === 'down' || (key.ctrl && key.name === 'n'))) return mover(1);
1024
+ if (key && key.name === 'pageup') return mover(-8);
1025
+ if (key && key.name === 'pagedown') return mover(8);
1026
+ if (key && (key.name === 'return' || key.name === 'enter')) {
1027
+ const item = this._itensDoDialogo()[d.sel];
1028
+ this.dialogo = null; this._quadroAnterior = null;
1029
+ this._pintar();
1030
+ if (item && d.aoEscolher) d.aoEscolher(item, this);
1031
+ return;
1032
+ }
1033
+ if (key && key.name === 'backspace') { d.filtro = d.filtro.slice(0, -1); d.sel = 0; return this._pintar(); }
1034
+ if (seq && !(key && (key.ctrl || key.meta)) && seq >= ' ' && seq !== '\x7f') { d.filtro += seq; d.sel = 0; this._pintar(); }
1035
+ }
1036
+
1037
+ /** Roda um item da paleta: comando de barra vai como se tivesse sido digitado; função é chamada. */
1038
+ _rodarItem(it) {
1039
+ if (!it) return;
1040
+ if (typeof it.acao === 'function') return it.acao(this);
1041
+ const cmd = it.cmd || it.nome;
1042
+ if (it.preencher) { this.line = cmd; this.cursor = cmd.length; return this._redesenhar(); }
1043
+ this.line = cmd; this.cursor = cmd.length;
1044
+ this._enviar();
1045
+ }
1046
+
1047
+ /** A tecla depois do ctrl+x. */
1048
+ _atalhoDoLider(key) {
1049
+ const d = this.op.dialogos || {};
1050
+ const abrir = (nome) => { if (d[nome]) Promise.resolve(d[nome](this)).then((def) => { if (def) this.abrirDialogo(def); }); };
1051
+ switch (key.name) {
1052
+ case 'n': return this._rodarItem({ cmd: '/clear' });
1053
+ case 'p': return this._rodarItem({ cmd: '/projects' });
1054
+ case 's': return this._rodarItem({ cmd: '/estudio' });
1055
+ case 'm': return abrir('modelo');
1056
+ case 'a': return abrir('agente');
1057
+ case 'l': return abrir('sessoes');
1058
+ case 't': return abrir('tema');
1059
+ case 'b': this.lateralOculta = !this.lateralOculta; this._quadroAnterior = null; return this._pintar();
1060
+ default: return;
1061
+ }
1062
+ }
1063
+
1064
+ /** Um aviso no canto de cima por alguns segundos. tipo: info · ok · aviso · erro. */
1065
+ toast(texto, tipo = 'info', ms = 3000) {
1066
+ this.toastAtual = { texto: String(texto || ''), tipo };
1067
+ clearTimeout(this._toastTempo);
1068
+ this._toastTempo = setTimeout(() => { this.toastAtual = null; this._quadroAnterior = null; this._agendar(); }, ms);
1069
+ if (this._toastTempo.unref) this._toastTempo.unref();
1070
+ if (this.cheia) this._agendar();
1071
+ }
1072
+
948
1073
  _novaLinha() {
949
1074
  this.line = this.line.slice(0, this.cursor) + '\n' + this.line.slice(this.cursor);
950
1075
  this.cursor++;
@@ -1010,7 +1135,7 @@ class Entrada extends EventEmitter {
1010
1135
  // painel da caixa, com a barra, e um respiro em cima e embaixo.
1011
1136
  if (this.cheia) {
1012
1137
  const W = this._colunasReais || 80;
1013
- const mainW = W >= tela.LATERAL_MIN ? W - tela.LATERAL_W : W;
1138
+ const mainW = W - tela.larguraLateral(W, this.lateralOculta);
1014
1139
  this.rolagem = 0;
1015
1140
  /* Uma linha em branco antes e outra depois do pedido: cada
1016
1141
  troca (pedido, resposta) é um bloco separado na tela. */