primocode 11.1.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.
- package/README.md +16 -2
- package/bin/primocode-rpc.js +100 -0
- package/lib/agentes-pessoais.js +120 -0
- package/lib/app-server/cli.js +64 -0
- package/lib/app-server/enderecos.js +43 -0
- package/lib/app-server/index.js +3 -0
- package/lib/app-server/pareamento.js +61 -0
- package/lib/app-server/protocolo.js +86 -0
- package/lib/app-server/servidor.js +805 -0
- package/lib/app-server/websocket.js +87 -0
- package/lib/avisos.js +65 -0
- package/lib/cli/sessao.js +202 -17
- package/lib/cli-nova.js +237 -0
- package/lib/config/config.js +22 -2
- package/lib/diff.js +12 -2
- package/lib/motor/aprovacao.js +114 -0
- package/lib/motor/eventos.js +77 -0
- package/lib/motor/index.js +8 -0
- package/lib/motor/tela-terminal.js +64 -0
- package/lib/motor/transcricao.js +57 -0
- package/lib/motor/turno.js +43 -0
- package/lib/motor/voz.js +65 -0
- package/lib/piloto.js +39 -0
- package/lib/plugin/entregaveis/nucleo.js +2 -2
- package/lib/plugin/estudio/criar.js +113 -0
- package/lib/plugin/estudio/gosto.js +3 -0
- package/lib/plugin/estudio/historico.js +60 -0
- package/lib/plugin/estudio/index.js +14 -1
- package/lib/plugin/estudio/kits/tela.html +3 -1
- package/lib/plugin/estudio/miniatura.js +56 -0
- package/lib/plugin/video/auto.js +1 -1
- package/lib/plugin/video/catalogo.js +20 -0
- package/lib/plugin/video/mutar.js +28 -0
- package/lib/plugin/video/ops-fx.js +6 -6
- package/lib/plugin/video/ops.js +2 -13
- package/lib/plugin/video/pedido.js +1 -1
- package/lib/plugin/video/zoom.js +1 -1
- package/lib/projetos-meta.js +107 -0
- package/lib/qr.js +209 -0
- package/lib/session/runner/act.js +127 -68
- package/lib/skill/catalogo.js +3 -1
- package/lib/tool/tools.js +7 -0
- package/lib/tui/entrada.js +143 -18
- package/lib/tui/tela.js +312 -49
- package/lib/tui/ui.js +78 -4
- package/package.json +10 -2
- package/voz/primo_voz/transcrever.py +12 -0
package/lib/tui/entrada.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
540
|
-
if (
|
|
550
|
+
const detalhe = agora || (this.spinnerTexto ? stripAnsiLocal(this.spinnerTexto).replace(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s*/, '') : '');
|
|
551
|
+
if (detalhe) pe.push(c.dim(detalhe));
|
|
541
552
|
} else {
|
|
542
|
-
|
|
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,
|
|
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
|
|
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
|
|
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. */
|