primocode 11.2.0 → 11.3.1
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 +2 -2
- package/lib/agentes-pessoais.js +5 -2
- package/lib/app-server/cli.js +6 -0
- package/lib/app-server/enderecos.js +43 -0
- package/lib/app-server/servidor.js +174 -31
- package/lib/avisos.js +1 -0
- package/lib/cli/sessao.js +174 -16
- package/lib/config/config.js +16 -1
- package/lib/diff.js +12 -2
- package/lib/motor/fala-viva.js +77 -0
- package/lib/motor/processo-vivo.js +66 -0
- package/lib/motor/transcricao.js +81 -0
- package/lib/plugin/entregaveis/nucleo.js +2 -2
- package/lib/plugin/estudio/gosto.js +3 -0
- package/lib/plugin/estudio/kits/tela.html +3 -1
- package/lib/projetos-meta.js +20 -3
- package/lib/session/runner/act.js +23 -4
- package/lib/tool/tools.js +1 -0
- package/lib/tui/entrada.js +143 -18
- package/lib/tui/tela.js +312 -49
- package/lib/tui/ui.js +77 -3
- package/package.json +6 -2
- package/studio/voz_kokoro_vivo.py +46 -0
- package/voz/primo_voz/transcrever.py +37 -0
package/lib/projetos-meta.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
|
|
9
|
-
const ICONES = ['folder', 'film', 'palette', 'workflow', 'pen', 'home', 'shield', 'bell', 'sliders'];
|
|
9
|
+
const ICONES = ['folder', 'film', 'palette', 'workflow', 'pen', 'home', 'shield', 'bell', 'sliders', 'code', 'rocket', 'cake', 'store', 'clapperboard', 'megaphone', 'graduation-cap', 'heart', 'camera', 'music'];
|
|
10
10
|
const COR_PADRAO = '#22d3ee';
|
|
11
11
|
const hexOk = (h) => /^#[0-9a-f]{6}$/i.test(String(h || ''));
|
|
12
12
|
const cfg = () => require('./config/config');
|
|
@@ -36,6 +36,21 @@ function ramoGit(dir) {
|
|
|
36
36
|
} catch { return { git: false, ramo: '' }; }
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/* O "dono/repo" do remoto origin, lido do .git/config (sem rodar o git). Vazio se não há. */
|
|
40
|
+
function remotoGit(dir) {
|
|
41
|
+
try {
|
|
42
|
+
const cfg = fs.readFileSync(path.join(dir, '.git', 'config'), 'utf8');
|
|
43
|
+
const m = /\[remote "origin"\][^\[]*?url\s*=\s*(\S+)/.exec(cfg);
|
|
44
|
+
const r = m && /[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/.exec(m[1]);
|
|
45
|
+
return r ? r[1] : '';
|
|
46
|
+
} catch { return ''; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Quantos itens (arquivos e pastas, sem os ocultos) o projeto tem na raiz, e quando mexeram nele por último. */
|
|
50
|
+
function conteudo(dir) {
|
|
51
|
+
try { return { itens: fs.readdirSync(dir).filter((n) => !n.startsWith('.')).length, atualizado: fs.statSync(dir).mtimeMs }; } catch { return { itens: 0, atualizado: null }; }
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
function resumo(id) {
|
|
40
55
|
const dir = path.join(raiz(), id);
|
|
41
56
|
const m = lerMeta(dir);
|
|
@@ -43,12 +58,14 @@ function resumo(id) {
|
|
|
43
58
|
id, nome: m.nome || id, descricao: m.descricao || '',
|
|
44
59
|
icone: ICONES.includes(m.icone) ? m.icone : 'folder', cor: hexOk(m.cor) ? m.cor : COR_PADRAO,
|
|
45
60
|
caminho: dir, arquivado: !!m.arquivado, criado: m.criado || null,
|
|
46
|
-
...ramoGit(dir),
|
|
61
|
+
...ramoGit(dir), remoto: remotoGit(dir), ...conteudo(dir),
|
|
47
62
|
};
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
function listar({ arquivados = false } = {}) {
|
|
51
|
-
|
|
66
|
+
/* O mais recente primeiro (como a lista da barra lateral); empate por nome. */
|
|
67
|
+
return cfg().listProjects().map(resumo).filter((p) => arquivados || !p.arquivado)
|
|
68
|
+
.sort((a, b) => String(b.criado || '').localeCompare(String(a.criado || '')) || a.nome.localeCompare(b.nome));
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
function limpar(c, { criando = false } = {}) {
|
|
@@ -246,6 +246,17 @@ function ident(profundidade) {
|
|
|
246
246
|
* duas versões do texto, que o `limparParaOModelo` apaga antes de a conversa
|
|
247
247
|
* seguir. Sem essa limpeza, cada edição mandaria o arquivo inteiro de volta
|
|
248
248
|
* ao modelo — que acabou de escrevê-lo e não aprende nada relendo. */
|
|
249
|
+
/* +N −M de uma escrita ou edição: o diff de verdade (linhas que entraram e saíram). Arquivo
|
|
250
|
+
novo conta todas as linhas como entradas. Sem as duas versões nem a contagem, null. */
|
|
251
|
+
function contarMudanca(result) {
|
|
252
|
+
if (!result) return null;
|
|
253
|
+
if (typeof result._antes === 'string' && typeof result._depois === 'string') {
|
|
254
|
+
try { return diff.contar(diff.operacoes(result._antes, result._depois)); } catch { return null; }
|
|
255
|
+
}
|
|
256
|
+
if (result.created && Number.isFinite(result.linhas)) return { mais: result.linhas, menos: 0 };
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
249
260
|
function mostrarDiff(result, profundidade) {
|
|
250
261
|
if (!result || typeof result._antes !== 'string' || typeof result._depois !== 'string') return;
|
|
251
262
|
const bloco = diff.desenhar(result._antes, result._depois, {
|
|
@@ -287,17 +298,24 @@ function logResult(kind, result, profundidade) {
|
|
|
287
298
|
}
|
|
288
299
|
|
|
289
300
|
switch (kind) {
|
|
290
|
-
case 'write_file':
|
|
291
|
-
|
|
301
|
+
case 'write_file': {
|
|
302
|
+
const k = contarMudanca(result);
|
|
303
|
+
if (k) require('../../tui/ui').registrarArquivo(result.path, k.mais, k.menos);
|
|
304
|
+
console.log(p + c.muted(`${result.created ? 'criado' : 'atualizado'} · ${tools.fmtBytes(result.bytes)}`)
|
|
305
|
+
+ (k ? c.muted(' · ') + c.ok(`+${k.mais}`) + ' ' + c.err(`−${k.menos}`) : ''));
|
|
292
306
|
// Reescrever arquivo que já existia apaga o que estava lá. O
|
|
293
307
|
// número de bytes não conta isso; o bloco embaixo conta.
|
|
294
308
|
mostrarDiff(result, profundidade);
|
|
295
309
|
return;
|
|
310
|
+
}
|
|
296
311
|
case 'edit_file':
|
|
297
312
|
case 'multi_edit': {
|
|
298
313
|
const delta = result.linhasDepois - result.linhasAntes;
|
|
299
314
|
const sinal = delta > 0 ? c.ok(`+${delta}`) : delta < 0 ? c.err(`${delta}`) : c.muted('±0');
|
|
300
|
-
|
|
315
|
+
const k = contarMudanca(result);
|
|
316
|
+
if (k) require('../../tui/ui').registrarArquivo(result.path, k.mais, k.menos);
|
|
317
|
+
console.log(p + c.muted(`${result.substituicoes} troca${result.substituicoes > 1 ? 's' : ''} · `) + sinal + c.muted(' linhas')
|
|
318
|
+
+ (k ? c.muted(' · ') + c.ok(`+${k.mais}`) + ' ' + c.err(`−${k.menos}`) : ''));
|
|
301
319
|
mostrarDiff(result, profundidade);
|
|
302
320
|
return;
|
|
303
321
|
}
|
|
@@ -342,6 +360,7 @@ function logResult(kind, result, profundidade) {
|
|
|
342
360
|
return;
|
|
343
361
|
}
|
|
344
362
|
case 'write_todos': {
|
|
363
|
+
require('../../tui/ui').estadoSessao.todos = result.todos || [];
|
|
345
364
|
const marca = { completed: c.ok('✔'), in_progress: c.brand('▶'), pending: c.dim('○') };
|
|
346
365
|
for (const t of result.todos || []) {
|
|
347
366
|
const texto = t.status === 'completed' ? c.dim(t.content) : c.white(t.content);
|
|
@@ -2287,7 +2306,7 @@ async function runActInterno(userPrompt, opts) {
|
|
|
2287
2306
|
// arquivo de verdade — a memória era descartada nesse caso, sempre.
|
|
2288
2307
|
// A raiz de trabalho não é projeto: um memory.md solto em ~/primocode
|
|
2289
2308
|
// era o que um pedido de vídeo (que trabalha na raiz) deixava para trás.
|
|
2290
|
-
if (!subagent && temPasta && require('path').resolve(pastaAtual) !== require('path').resolve(cfg.ROOT_DIR)) {
|
|
2309
|
+
if (!subagent && temPasta && pastaAtual && require('path').resolve(pastaAtual) !== require('path').resolve(cfg.ROOT_DIR)) {
|
|
2291
2310
|
cfg.appendProjectMemory(pastaAtual, passagemDaParada || `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
|
|
2292
2311
|
}
|
|
2293
2312
|
|
package/lib/tool/tools.js
CHANGED
|
@@ -688,6 +688,7 @@ async function despachar(name, args, ctx) {
|
|
|
688
688
|
ok: true, created,
|
|
689
689
|
path: path.relative(projectDir, filePath).replace(/\\/g, '/'),
|
|
690
690
|
bytes: content.length,
|
|
691
|
+
linhas: content.split('\n').length,
|
|
691
692
|
...(created ? {} : { _antes: antes, _depois: content }),
|
|
692
693
|
};
|
|
693
694
|
}
|
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. */
|