primocode 9.10.0 → 10.0.9
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 +30 -6
- package/bin/primocode.js +169 -342
- package/lib/act.js +360 -39
- package/lib/browser.js +105 -4
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +77 -11
- package/lib/estudio/cinema.js +27 -9
- package/lib/estudio/diretor.js +21 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +66 -0
- package/lib/estudio/gerativo.js +1453 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/hibrido.js +165 -0
- package/lib/estudio/index.js +284 -53
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/gerativo.jsx +980 -0
- package/lib/estudio/kit/index.jsx +13 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/tres.jsx +308 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +78 -0
- package/lib/estudio/lancamento.js +441 -0
- package/lib/estudio/motion.js +2 -5
- package/lib/estudio/plano.js +28 -3
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +139 -51
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variedade.js +105 -0
- package/lib/estudio/vox.js +414 -0
- package/lib/fala.js +1 -94
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +44 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +100 -22
- package/lib/ui.js +50 -8
- package/lib/video/analise.js +89 -0
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +280 -0
- package/lib/video/fx3d/Camadas.jsx +64 -0
- package/lib/video/fx3d/Objetos.jsx +128 -0
- package/lib/video/fx3d/Root.jsx +18 -0
- package/lib/video/fx3d/modelos.jsx +304 -0
- package/lib/video/fx3d.js +108 -0
- package/lib/video/gerados.js +119 -0
- package/lib/video/objetos.js +111 -0
- package/lib/video/ops-fx.js +453 -0
- package/lib/video/ops.js +347 -0
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +274 -0
- package/lib/video/texto.js +144 -0
- package/lib/video/timeline.js +318 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +5 -5
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- package/voz/primo_voz/orbe.py +0 -406
package/lib/browser.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
|
+
const snapshot = require('./snapshot.js');
|
|
17
18
|
const fs = require('fs');
|
|
18
19
|
const os = require('os');
|
|
19
20
|
const net = require('net');
|
|
@@ -22,9 +23,36 @@ const http = require('http');
|
|
|
22
23
|
const crypto = require('crypto');
|
|
23
24
|
const { spawn, execSync } = require('child_process');
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
+
const PERFIL_VISIVEL = path.join(os.homedir(), '.primocode', 'chrome-profile');
|
|
26
27
|
// Dentro da faixa 3100-3199 do ambiente local, para o start.sh cuidar dela.
|
|
27
|
-
const
|
|
28
|
+
const PORTA_VISIVEL = Number(process.env.PRIMOCODE_CDP_PORT || 3199);
|
|
29
|
+
|
|
30
|
+
/* ── VISÍVEL SÓ QUANDO A PESSOA PEDIU ────────────────────────────────────
|
|
31
|
+
* "toda vez que eu mando alguma coisa pra ele ele abre umas 10.000 abas
|
|
32
|
+
* do Chrome — ele nem usa — e eu fecho e ele abre de novo" (Joel, 02/10)
|
|
33
|
+
* As ferramentas browser_* vão para o modelo em todo pedido, e ele as usa
|
|
34
|
+
* para PESQUISAR (o motion da Paris Group subiu o Chrome às 09:41 sem
|
|
35
|
+
* ninguém pedir navegador). Agora o terminal diz, por pedido, se a pessoa
|
|
36
|
+
* quer ver (definirVisivel): sem pedido de navegador, as mesmas ferramentas
|
|
37
|
+
* rodam num Chrome --headless de perfil e porta próprios, que nunca abre
|
|
38
|
+
* janela nem aba na tela — e morre com o processo. */
|
|
39
|
+
const PERFIL_ESCONDIDO = path.join(os.homedir(), '.primocode', 'chrome-escondido');
|
|
40
|
+
const PORTA_ESCONDIDA = Number(process.env.PRIMOCODE_CDP_PORT_ESCONDIDO || 3197);
|
|
41
|
+
let visivel = true;
|
|
42
|
+
let DEBUG_PORT = PORTA_VISIVEL;
|
|
43
|
+
let PROFILE_DIR = PERFIL_VISIVEL;
|
|
44
|
+
|
|
45
|
+
function definirVisivel(sim) {
|
|
46
|
+
const quer = sim !== false;
|
|
47
|
+
if (quer === visivel) return visivel;
|
|
48
|
+
visivel = quer;
|
|
49
|
+
DEBUG_PORT = quer ? PORTA_VISIVEL : PORTA_ESCONDIDA;
|
|
50
|
+
PROFILE_DIR = quer ? PERFIL_VISIVEL : PERFIL_ESCONDIDO;
|
|
51
|
+
// A sessão aberta é do outro navegador: a próxima ação conecta no certo.
|
|
52
|
+
if (currentSession) { try { currentSession.close(); } catch { /* já fechou */ } currentSession = null; }
|
|
53
|
+
return visivel;
|
|
54
|
+
}
|
|
55
|
+
function estaVisivel() { return visivel; }
|
|
28
56
|
|
|
29
57
|
// ---- Helpers ---------------------------------------------------
|
|
30
58
|
/* O Chromium que o Playwright baixa. Ele não fica no PATH nem em /Applications
|
|
@@ -413,12 +441,20 @@ async function ensureChromeRunning() {
|
|
|
413
441
|
args.push('--no-sandbox');
|
|
414
442
|
}
|
|
415
443
|
// Sem interface gráfica (CI, servidor): PRIMOCODE_BROWSER_HEADLESS=1.
|
|
416
|
-
|
|
417
|
-
|
|
444
|
+
// E sempre que a pessoa não pediu para ver (definirVisivel).
|
|
445
|
+
const escondido = !visivel;
|
|
446
|
+
if (escondido || process.env.PRIMOCODE_BROWSER_HEADLESS === '1' || process.env.PRIMOCODE_BROWSER_HEADLESS === 'true') {
|
|
447
|
+
args.push('--headless=new', '--disable-gpu', '--disable-dev-shm-usage', '--mute-audio');
|
|
418
448
|
}
|
|
419
449
|
args.push('about:blank');
|
|
420
450
|
chromeChild = spawn(chrome, args, { detached: true, stdio: 'ignore' });
|
|
421
451
|
chromeChild.on('error', () => {});
|
|
452
|
+
/* O escondido morre com o processo: ninguém o vê para fechar, e um
|
|
453
|
+
headless órfão por sessão era o que o revisor deixava em 24/09. */
|
|
454
|
+
if (escondido) {
|
|
455
|
+
const filho = chromeChild;
|
|
456
|
+
process.once('exit', () => { try { process.kill(-filho.pid); } catch { try { filho.kill(); } catch { /* já morreu */ } } });
|
|
457
|
+
}
|
|
422
458
|
chromeChild.unref();
|
|
423
459
|
|
|
424
460
|
// Espera a porta responder
|
|
@@ -491,6 +527,55 @@ async function open(url) {
|
|
|
491
527
|
return { ok: true, url };
|
|
492
528
|
}
|
|
493
529
|
|
|
530
|
+
/* ── ABRIR À VISTA ────────────────────────────────────────────────────────
|
|
531
|
+
* "Peço pra ele abrir o Chrome e o YouTube: em vez de só abrir, ele mostra
|
|
532
|
+
* o mouse dele abrindo o Chrome, pesquisando…" (30/09)
|
|
533
|
+
* O cursor do PrimoCode (voz/primo_voz/cursor.py, macOS) vai até a barra de
|
|
534
|
+
* endereço da janela de verdade, clica, digita o endereço letra por letra e
|
|
535
|
+
* aperta Enter — quem olha vê o computador sendo usado. A janela vem do CDP
|
|
536
|
+
* (Browser.getWindowForTarget), então o clique cai na barra mesmo com a
|
|
537
|
+
* janela em qualquer lugar da tela. Sem cursor (outro SO, sem permissão),
|
|
538
|
+
* navega direto, como antes. */
|
|
539
|
+
async function abrirVisivel(url) {
|
|
540
|
+
// Escondido não tem janela para o cursor ir: navega direto.
|
|
541
|
+
if (!visivel) return open(url);
|
|
542
|
+
await ensure();
|
|
543
|
+
const cursor = require('./cursor.js');
|
|
544
|
+
if (!verSeta() || !cursor.disponivel()) return open(url);
|
|
545
|
+
try { await currentSession.send('Page.bringToFront'); } catch { /* segue */ }
|
|
546
|
+
let janela = null;
|
|
547
|
+
try {
|
|
548
|
+
const alvo = await currentSession.send('Target.getTargetInfo');
|
|
549
|
+
const w = await currentSession.send('Browser.getWindowForTarget', alvo && alvo.targetInfo ? { targetId: alvo.targetInfo.targetId } : {});
|
|
550
|
+
janela = w && w.bounds;
|
|
551
|
+
} catch { janela = null; }
|
|
552
|
+
if (!janela || janela.left == null) return open(url);
|
|
553
|
+
// A barra fica uns 58 pt abaixo do topo da janela (abas + barra), a ~30% da largura.
|
|
554
|
+
const x = Math.round(janela.left + Math.min(420, janela.width * 0.3));
|
|
555
|
+
const y = Math.round(janela.top + 58);
|
|
556
|
+
const passos = [
|
|
557
|
+
['clicar', { x, y, botao: 'esquerdo', rotulo: 'barra de endereço' }],
|
|
558
|
+
['tecla', { combo: 'cmd+a' }],
|
|
559
|
+
['digitar', { texto: url.replace(/^https?:\/\//, ''), rotulo: 'digitando o endereço' }],
|
|
560
|
+
['tecla', { combo: 'enter' }],
|
|
561
|
+
];
|
|
562
|
+
for (const [cmd, args] of passos) {
|
|
563
|
+
const r = await cursor.mandar(cmd, args);
|
|
564
|
+
if (!r || r.ok === false) return open(url); // sem permissão de Acessibilidade: navega direto
|
|
565
|
+
}
|
|
566
|
+
const deadline = Date.now() + 20000;
|
|
567
|
+
await sleep(600);
|
|
568
|
+
while (Date.now() < deadline) {
|
|
569
|
+
try {
|
|
570
|
+
const r = await currentSession.send('Runtime.evaluate', { expression: 'document.readyState + "|" + location.href', returnByValue: true });
|
|
571
|
+
const [estado, href] = String((r.result && r.result.value) || '').split('|');
|
|
572
|
+
if (estado === 'complete' && href && href !== 'about:blank') return { ok: true, url: href, visivel: true };
|
|
573
|
+
} catch { /* a página está trocando */ }
|
|
574
|
+
await sleep(200);
|
|
575
|
+
}
|
|
576
|
+
return { ok: true, url, visivel: true };
|
|
577
|
+
}
|
|
578
|
+
|
|
494
579
|
async function evaluate(js) {
|
|
495
580
|
await ensure();
|
|
496
581
|
const r = await currentSession.send('Runtime.evaluate', {
|
|
@@ -658,7 +743,22 @@ async function candidatosNaPagina() {
|
|
|
658
743
|
try { return JSON.parse(await evaluate(js)) || []; } catch { return []; }
|
|
659
744
|
}
|
|
660
745
|
|
|
746
|
+
/* O retrato da página com as refs (lib/snapshot.js), já em texto. */
|
|
747
|
+
async function retrato(max = 80) {
|
|
748
|
+
try {
|
|
749
|
+
const r = await evaluate(snapshot.scriptDoRetrato(max));
|
|
750
|
+
return snapshot.formatar(r, max);
|
|
751
|
+
} catch { return ''; }
|
|
752
|
+
}
|
|
753
|
+
|
|
661
754
|
async function erroDeSeletor(selector) {
|
|
755
|
+
// Ref que não existe mais: a página mudou depois da leitura. Listar os
|
|
756
|
+
// seletores CSS aqui só confundiria — o caminho é ler de novo.
|
|
757
|
+
if (/data-primo-ref/.test(selector)) {
|
|
758
|
+
const err = new Error('essa ref não existe mais nesta página — ela mudou depois da leitura. '
|
|
759
|
+
+ 'Estas são as refs de agora:\n' + await retrato(40));
|
|
760
|
+
return err;
|
|
761
|
+
}
|
|
662
762
|
const candidatos = await candidatosNaPagina();
|
|
663
763
|
const err = new Error(
|
|
664
764
|
`seletor não encontrado: ${selector}`
|
|
@@ -1191,6 +1291,7 @@ function fecharRevisor() {
|
|
|
1191
1291
|
}
|
|
1192
1292
|
|
|
1193
1293
|
module.exports = {
|
|
1294
|
+
retrato, abrirVisivel, definirVisivel, estaVisivel,
|
|
1194
1295
|
SEM_KEYCHAIN,
|
|
1195
1296
|
scriptDaSeta, verSeta, findChrome,
|
|
1196
1297
|
open, click, clickText, clickSelector, type, typeInSelector,
|
package/lib/casar.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* casar.js — acha no arquivo o trecho que o modelo MANDOU trocar, mesmo
|
|
3
|
+
* quando ele não copiou letra por letra.
|
|
4
|
+
*
|
|
5
|
+
* O modelo grátis erra o old_string de jeitos previsíveis: um espaço a mais
|
|
6
|
+
* no fim da linha, a indentação com 2 em vez de 4, o número de linha que o
|
|
7
|
+
* read_file com offset põe na frente ("12\t…"), um "\n" escapado. Cada erro
|
|
8
|
+
* desses virava "trecho não encontrado" → read_file de novo → edit de novo:
|
|
9
|
+
* duas voltas de modelo para um espaço. É o "fica o dia inteiro lendo o que
|
|
10
|
+
* fez". A ideia vem do opencode (MIT), que encadeia vários "casadores" do
|
|
11
|
+
* mais estrito ao mais frouxo; o código é nosso.
|
|
12
|
+
*
|
|
13
|
+
* A regra de ouro: frouxo, mas NUNCA ambíguo. Cada estratégia só vale se
|
|
14
|
+
* achar exatamente UM lugar. Dois lugares parecidos é pior que nenhum —
|
|
15
|
+
* trocar o errado estraga o arquivo calado.
|
|
16
|
+
*
|
|
17
|
+
* achar(conteudo, alvo) → { inicio, fim, como } | { erro: 'nenhum' | 'varios', vezes }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
// Onde está cada linha no texto: [inicio, fim) de cada uma, sem o \n.
|
|
23
|
+
function linhasComPosicao(texto) {
|
|
24
|
+
const out = [];
|
|
25
|
+
let i = 0;
|
|
26
|
+
for (const l of texto.split('\n')) {
|
|
27
|
+
out.push({ texto: l, inicio: i, fim: i + l.length });
|
|
28
|
+
i += l.length + 1;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Procura um bloco de linhas do alvo no conteúdo comparando cada linha por
|
|
34
|
+
`igual`. Devolve todos os lugares (em caracteres do conteúdo original). */
|
|
35
|
+
function porLinhas(conteudo, alvo, normalizar) {
|
|
36
|
+
const doArquivo = linhasComPosicao(conteudo);
|
|
37
|
+
let doAlvo = alvo.split('\n');
|
|
38
|
+
// Linha vazia no começo ou no fim do alvo é quase sempre sobra do modelo.
|
|
39
|
+
while (doAlvo.length > 1 && !doAlvo[0].trim()) doAlvo = doAlvo.slice(1);
|
|
40
|
+
while (doAlvo.length > 1 && !doAlvo[doAlvo.length - 1].trim()) doAlvo = doAlvo.slice(0, -1);
|
|
41
|
+
const alvoN = doAlvo.map(normalizar);
|
|
42
|
+
if (!alvoN.some(Boolean)) return [];
|
|
43
|
+
const achados = [];
|
|
44
|
+
for (let i = 0; i + alvoN.length <= doArquivo.length; i++) {
|
|
45
|
+
let bate = true;
|
|
46
|
+
for (let j = 0; j < alvoN.length; j++) {
|
|
47
|
+
if (normalizar(doArquivo[i + j].texto) !== alvoN[j]) { bate = false; break; }
|
|
48
|
+
}
|
|
49
|
+
if (bate) achados.push({ inicio: doArquivo[i].inicio, fim: doArquivo[i + alvoN.length - 1].fim });
|
|
50
|
+
}
|
|
51
|
+
return achados;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Tira a indentação que todas as linhas não vazias têm em comum.
|
|
55
|
+
function semIndentacaoComum(linhas) {
|
|
56
|
+
const recuos = linhas.filter((l) => l.trim()).map((l) => l.match(/^[ \t]*/)[0].length);
|
|
57
|
+
const menor = recuos.length ? Math.min(...recuos) : 0;
|
|
58
|
+
return linhas.map((l) => l.slice(Math.min(menor, l.match(/^[ \t]*/)[0].length)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function porIndentacao(conteudo, alvo) {
|
|
62
|
+
const doArquivo = linhasComPosicao(conteudo);
|
|
63
|
+
const doAlvo = semIndentacaoComum(alvo.replace(/^\n+|\n+$/g, '').split('\n')).map((l) => l.trimEnd());
|
|
64
|
+
const n = doAlvo.length;
|
|
65
|
+
if (!doAlvo.some((l) => l.trim())) return [];
|
|
66
|
+
const achados = [];
|
|
67
|
+
for (let i = 0; i + n <= doArquivo.length; i++) {
|
|
68
|
+
const janela = semIndentacaoComum(doArquivo.slice(i, i + n).map((l) => l.texto)).map((l) => l.trimEnd());
|
|
69
|
+
if (janela.every((l, j) => l === doAlvo[j])) {
|
|
70
|
+
achados.push({ inicio: doArquivo[i].inicio, fim: doArquivo[i + n - 1].fim });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return achados;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ocorrencias(conteudo, alvo) {
|
|
77
|
+
const out = [];
|
|
78
|
+
if (!alvo) return out;
|
|
79
|
+
let i = conteudo.indexOf(alvo);
|
|
80
|
+
while (i !== -1) { out.push({ inicio: i, fim: i + alvo.length }); i = conteudo.indexOf(alvo, i + alvo.length); }
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Do mais estrito ao mais frouxo. O primeiro que achar UM lugar vence; o que
|
|
85
|
+
achar vários para a busca ali mesmo (ficar mais frouxo só pioraria). */
|
|
86
|
+
const ESTRATEGIAS = [
|
|
87
|
+
['exato', (c, a) => ocorrencias(c, a)],
|
|
88
|
+
// "12\t <div>" — o prefixo que o read_file com offset põe em cada linha.
|
|
89
|
+
['sem número de linha', (c, a) => (/^\s*\d+\t/m.test(a) ? ocorrencias(c, a.replace(/^\s*\d+\t/gm, '')) : [])],
|
|
90
|
+
// "\\n" e "\\t" que vieram escapados de um JSON mal montado.
|
|
91
|
+
['sem escape', (c, a) => (/\\[nt"]/.test(a) && !/\\[nt"]/.test(c)
|
|
92
|
+
? ocorrencias(c, a.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\"/g, '"')) : [])],
|
|
93
|
+
['espaço no fim da linha', (c, a) => porLinhas(c, a, (l) => l.trimEnd())],
|
|
94
|
+
['linha aparada', (c, a) => porLinhas(c, a, (l) => l.trim())],
|
|
95
|
+
['indentação', (c, a) => porIndentacao(c, a)],
|
|
96
|
+
['espaço normalizado', (c, a) => porLinhas(c, a, (l) => l.replace(/\s+/g, ' ').trim())],
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
function achar(conteudo, alvo) {
|
|
100
|
+
for (const [como, f] of ESTRATEGIAS) {
|
|
101
|
+
const achados = f(conteudo, alvo);
|
|
102
|
+
if (achados.length === 1) return { ...achados[0], como };
|
|
103
|
+
if (achados.length > 1) return { erro: 'varios', vezes: achados.length, como };
|
|
104
|
+
}
|
|
105
|
+
return { erro: 'nenhum' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { achar, ocorrencias };
|
package/lib/catalogo.js
CHANGED
|
@@ -87,12 +87,16 @@ const GRUPOS = [
|
|
|
87
87
|
args: 'id. PREFIRA roteiro: {marca:{cores:{fundo,destaque,tinta},fontes:{titulo,texto},logo}, proporcao, '
|
|
88
88
|
+ 'partes:[{tipo, fala, …}]} — o PrimoCode faz o layout, a voz com entonação, os ícones, a música e os sons. '
|
|
89
89
|
+ 'Tipos: abertura{titulo,sub} titulo{titulo,sub} numero{valor,rotulo,sub} lista{titulo,itens} passos{titulo,itens} '
|
|
90
|
-
+ 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link}
|
|
90
|
+
+ 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link} '
|
|
91
|
+
+ 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). '
|
|
92
|
+
+ 'estilo: hibrido (PADRÃO: foto e vídeo reais que não param + o motion por cima; dê busca:"termo em inglês" por parte para o b-roll) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; é o do modo sem referências e o de quem pede motion/animação; com modelo3d o produto 3D entra numa batida) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
|
|
93
|
+
+ 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | impacto | cinema. '
|
|
94
|
+
+ 'marca e lancamento aceitam visual:orbe|vetor|estrela|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
|
|
91
95
|
+ 'voz (thalita|antonio|francisca). Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
|
|
92
96
|
+ 'cena (opcional): o número de UMA cena para trocar só ela, sem reescrever o resto. '
|
|
93
97
|
+ 'motivo (opcional): fica no caderno. voltarPara (opcional): nome de uma versão. '
|
|
94
98
|
+ 'Sem plano nem cenas, LÊ o plano atual. '
|
|
95
|
-
+ 'Cada elemento é {tipo: texto|forma|imagem|video|icone|legenda|componente} com x, y, cor, tamanho e '
|
|
99
|
+
+ 'Cada elemento é {tipo: texto|forma|imagem|video|icone|legenda|componente|kit} com x, y, cor, tamanho e '
|
|
96
100
|
+ '"anima": {propriedade: [{t,v,e}]} — quadros-chave em segundos. A cena tem "camera": {zoom,x,y,rotacao} e '
|
|
97
101
|
+ '"perspectiva" para 3D. NÃO existe layout pronto nem tema — cada propriedade do vídeo é escolha sua, a cada vídeo.' },
|
|
98
102
|
|
package/lib/claude-engine.js
CHANGED
|
@@ -575,6 +575,40 @@ function promptDoProtocolo(payload) {
|
|
|
575
575
|
+ '\n\nResponda com exatamente UM bloco marcado.';
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
+
/* O `claude -p` ASSÍNCRONO. Era execFileSync: enquanto o Claude pensava (um
|
|
579
|
+
minuto, dois), o PrimoCode inteiro parava — sem spinner, sem recado, sem
|
|
580
|
+
Esc Esc. E o teto de 180 s estourava justo quando ele escrevia um arquivo
|
|
581
|
+
grande, e o erro saía cru na tela ("Command failed: claude -p …"). Agora
|
|
582
|
+
roda em paralelo, com 10 min de teto, uma segunda tentativa, e o erro
|
|
583
|
+
diz o que o Claude disse. */
|
|
584
|
+
function rodarClaude(args, input, { timeout = 600000, tentativas = 2 } = {}) {
|
|
585
|
+
const { spawn } = require('child_process');
|
|
586
|
+
const umaVez = () => new Promise((resolve, reject) => {
|
|
587
|
+
const p = spawn('claude', args, { env: process.env, stdio: ['pipe', 'pipe', 'pipe'], ...(SEM_APARECER.windowsHide ? { windowsHide: true } : {}) });
|
|
588
|
+
let out = '', err = '';
|
|
589
|
+
const relogio = setTimeout(() => { try { p.kill(); } catch {} reject(new Error(`o Claude Code não respondeu em ${Math.round(timeout / 60000)} min`)); }, timeout);
|
|
590
|
+
p.stdout.on('data', (d) => { out += d; });
|
|
591
|
+
p.stderr.on('data', (d) => { err += d; });
|
|
592
|
+
p.on('error', (e) => { clearTimeout(relogio); reject(e); });
|
|
593
|
+
p.on('close', (code) => {
|
|
594
|
+
clearTimeout(relogio);
|
|
595
|
+
if (code === 0) return resolve(out);
|
|
596
|
+
// O JSON às vezes vem com is_error e o motivo, mesmo com código != 0.
|
|
597
|
+
let motivo = '';
|
|
598
|
+
try { const j = JSON.parse(out); motivo = j.result || j.error || ''; } catch { /* sem json */ }
|
|
599
|
+
reject(new Error(`o Claude Code saiu com ${code}: ${String(motivo || err || out).replace(/\s+/g, ' ').trim().slice(0, 300)}`));
|
|
600
|
+
});
|
|
601
|
+
p.stdin.end(input);
|
|
602
|
+
});
|
|
603
|
+
return (async () => {
|
|
604
|
+
let ultimo;
|
|
605
|
+
for (let i = 0; i < tentativas; i++) {
|
|
606
|
+
try { return await umaVez(); } catch (e) { ultimo = e; }
|
|
607
|
+
}
|
|
608
|
+
throw ultimo;
|
|
609
|
+
})();
|
|
610
|
+
}
|
|
611
|
+
|
|
578
612
|
async function requestActViaHeadless(payload, onEvent) {
|
|
579
613
|
const corpoBootstrap = corpoDoProtocolo();
|
|
580
614
|
const prompt = promptDoProtocolo(payload);
|
|
@@ -585,27 +619,14 @@ async function requestActViaHeadless(payload, onEvent) {
|
|
|
585
619
|
// O prompt vai por STDIN, nunca por argumento: argumento tem limite de
|
|
586
620
|
// tamanho (a conversa cresce) e qualquer conteúdo começando com traço
|
|
587
621
|
// seria lido como opção. stdin não tem nenhum dos dois problemas.
|
|
588
|
-
const saida =
|
|
589
|
-
'-p',
|
|
590
|
-
'--output-format', 'json',
|
|
591
|
-
'--dangerously-skip-permissions',
|
|
592
|
-
...argsDoMotor(),
|
|
593
|
-
], { encoding: 'utf-8', timeout: 180000, input: prompt, maxBuffer: 32 * 1024 * 1024,
|
|
594
|
-
env: process.env, ...SEM_APARECER });
|
|
622
|
+
const saida = await rodarClaude(['-p', '--output-format', 'json', '--dangerously-skip-permissions', ...argsDoMotor()], prompt);
|
|
595
623
|
const r = JSON.parse(saida);
|
|
596
624
|
// A correção é uma segunda ida ao motor, com o mesmo protocolo e o
|
|
597
625
|
// diagnóstico do que saiu errado. Barata: o prompt é curto, e evita que
|
|
598
626
|
// um erro de sintaxe do motor vire erro na cara do usuário.
|
|
599
|
-
const pedirCorrecao = (aviso) => {
|
|
600
|
-
const s =
|
|
601
|
-
|
|
602
|
-
...argsDoMotor(),
|
|
603
|
-
], {
|
|
604
|
-
encoding: 'utf-8', timeout: 120000, maxBuffer: 32 * 1024 * 1024,
|
|
605
|
-
env: process.env,
|
|
606
|
-
input: promptDeCorrecao(aviso),
|
|
607
|
-
...SEM_APARECER,
|
|
608
|
-
});
|
|
627
|
+
const pedirCorrecao = async (aviso) => {
|
|
628
|
+
const s = await rodarClaude(['-p', '--output-format', 'json', '--dangerously-skip-permissions', ...argsDoMotor()],
|
|
629
|
+
promptDeCorrecao(aviso), { timeout: 300000, tentativas: 1 });
|
|
609
630
|
return JSON.parse(s).result || '';
|
|
610
631
|
};
|
|
611
632
|
await emitirDecisao(r.result || '', onEvent, pedirCorrecao, 0, nomesDasTools(payload));
|
package/lib/codex-engine.js
CHANGED
|
@@ -131,22 +131,40 @@ function recusouOModelo(texto) {
|
|
|
131
131
|
* `--model` e avisa — cair para o padrão da conta é melhor que a tarefa
|
|
132
132
|
* morrer por causa de um nome que mudou de catálogo.
|
|
133
133
|
*/
|
|
134
|
-
|
|
134
|
+
/* O Codex ASSÍNCRONO, pelo mesmo motivo do claude -p (lib/claude-engine.js):
|
|
135
|
+
o execFileSync congelava o PrimoCode inteiro — spinner, recado, Esc Esc —
|
|
136
|
+
enquanto o Codex pensava. O erro mantém stdout/stderr para o diagnóstico. */
|
|
137
|
+
function rodarAssincrono(args, entrada) {
|
|
138
|
+
const { spawn } = require('child_process');
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const p = spawn(CODEX, args, { env: process.env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
141
|
+
let out = '', err = '';
|
|
142
|
+
const relogio = setTimeout(() => { try { p.kill(); } catch {} const e = new Error(`o Codex não respondeu em ${Math.round(TEMPO / 60000)} min`); e.stderr = err; e.stdout = out; reject(e); }, TEMPO);
|
|
143
|
+
p.stdout.on('data', (d) => { out += d; if (out.length > BUFFER) { try { p.kill(); } catch {} } });
|
|
144
|
+
p.stderr.on('data', (d) => { err += d; });
|
|
145
|
+
p.on('error', (e) => { clearTimeout(relogio); reject(e); });
|
|
146
|
+
p.on('close', (code) => {
|
|
147
|
+
clearTimeout(relogio);
|
|
148
|
+
if (code === 0) return resolve(out);
|
|
149
|
+
const e = new Error(`Command failed: codex (saiu com ${code})`); e.stderr = err; e.stdout = out; reject(e);
|
|
150
|
+
});
|
|
151
|
+
p.stdin.end(entrada);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function chamarCodex(prompt, { executor, avisar = () => {} } = {}) {
|
|
135
156
|
// A chave própria da pessoa, quando ela configurou uma. Sem configuração
|
|
136
157
|
// o ambiente vai INTACTO — passar a variável vazia apagaria a chave que
|
|
137
158
|
// ela já tem exportada no shell, e o Codex pararia de funcionar por ter
|
|
138
159
|
// sido "configurado".
|
|
139
|
-
const rodar = executor ||
|
|
140
|
-
encoding: 'utf-8', timeout: TEMPO, input: entrada, maxBuffer: BUFFER,
|
|
141
|
-
env: process.env, ...SEM_APARECER,
|
|
142
|
-
}));
|
|
160
|
+
const rodar = executor || rodarAssincrono;
|
|
143
161
|
try {
|
|
144
|
-
return rodar(argsDoExec(), prompt);
|
|
162
|
+
return await rodar(argsDoExec(), prompt);
|
|
145
163
|
} catch (e) {
|
|
146
164
|
const detalhe = `${e.message || ''}\n${(e.stderr || '')}\n${(e.stdout || '')}`;
|
|
147
165
|
if (MODELO_CODEX && recusouOModelo(detalhe)) {
|
|
148
166
|
avisar(`o Codex não aceitou o modelo ${MODELO_CODEX} — indo com o padrão da sua conta`);
|
|
149
|
-
return rodar(argsDoExec({ comModelo: false }), prompt);
|
|
167
|
+
return await rodar(argsDoExec({ comModelo: false }), prompt);
|
|
150
168
|
}
|
|
151
169
|
throw new Error(primeiraLinhaUtil(detalhe) || e.message);
|
|
152
170
|
}
|
|
@@ -185,12 +203,12 @@ async function requestAct(server, payload, onEvent) {
|
|
|
185
203
|
return;
|
|
186
204
|
}
|
|
187
205
|
const prompt = claude.promptDoProtocolo(payload);
|
|
188
|
-
const saida = chamarCodex(prompt, { avisar: (m) => onEvent({ aviso: m }) });
|
|
206
|
+
const saida = await chamarCodex(prompt, { avisar: (m) => onEvent({ aviso: m }) });
|
|
189
207
|
|
|
190
208
|
// A correção é uma segunda ida ao motor, com o mesmo protocolo e o
|
|
191
209
|
// diagnóstico do que saiu errado. Barata, e evita que um erro de forma
|
|
192
210
|
// do motor vire erro na cara de quem pediu a tarefa.
|
|
193
|
-
const pedirCorrecao = (aviso) => chamarCodex(claude.promptDeCorrecao(aviso));
|
|
211
|
+
const pedirCorrecao = (aviso) => chamarCodex(claude.promptDeCorrecao(aviso)); // devolve promessa: quem chama dá await
|
|
194
212
|
|
|
195
213
|
await claude.emitirDecisao(saida, onEvent, pedirCorrecao, 0,
|
|
196
214
|
claude.nomesDasTools(payload));
|
package/lib/config.js
CHANGED
|
@@ -74,8 +74,11 @@ function ensureDir(dir) {
|
|
|
74
74
|
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// O dia LOCAL: toISOString é UTC, e no Brasil depois das 21h a memória saía
|
|
78
|
+
// datada no dia seguinte.
|
|
77
79
|
function today() {
|
|
78
|
-
|
|
80
|
+
const d = new Date();
|
|
81
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
function ensureStructure() {
|
|
@@ -99,12 +102,16 @@ function slugify(name) {
|
|
|
99
102
|
.toLowerCase()
|
|
100
103
|
.replace(/[^a-z0-9]+/g, '-')
|
|
101
104
|
.replace(/^-+|-+$/g, '')
|
|
102
|
-
.slice(0, 60)
|
|
105
|
+
.slice(0, 60)
|
|
106
|
+
.replace(/-+$/, ''); // o corte pode deixar hífen no fim
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
function createProjectDir(projectName) {
|
|
106
110
|
ensureStructure();
|
|
107
|
-
|
|
111
|
+
/* Nome sem letra nem número ("!!!", só emoji, só japonês) dava slug
|
|
112
|
+
VAZIO — e path.join(ROOT_DIR, '') é a própria raiz: o "projeto" virava
|
|
113
|
+
~/primocode, com memory.md gravado nela. */
|
|
114
|
+
const slug = slugify(projectName) || `projeto-${today()}`;
|
|
108
115
|
const projectDir = path.join(ROOT_DIR, slug);
|
|
109
116
|
ensureDir(projectDir);
|
|
110
117
|
const memoryPath = path.join(projectDir, 'memory.md');
|
package/lib/cursor.js
CHANGED
|
@@ -95,7 +95,16 @@ function subir() {
|
|
|
95
95
|
};
|
|
96
96
|
proc.on('exit', morreu);
|
|
97
97
|
proc.on('error', morreu);
|
|
98
|
-
|
|
98
|
+
// Demorou demais: desiste E mata — senão o Python ficava órfão, e o
|
|
99
|
+
// `pronto` guardado como false deixava o cursor fora a sessão toda.
|
|
100
|
+
const teto = setTimeout(() => {
|
|
101
|
+
if (subiu) return;
|
|
102
|
+
semCursor = 'o cursor demorou demais para abrir';
|
|
103
|
+
try { proc && proc.kill(); } catch { /* já morreu */ }
|
|
104
|
+
pronto = null;
|
|
105
|
+
resolve(false);
|
|
106
|
+
}, 20000);
|
|
107
|
+
if (teto.unref) teto.unref();
|
|
99
108
|
// O cursor não pode segurar o PrimoCode aberto na saída.
|
|
100
109
|
proc.unref();
|
|
101
110
|
proc.stdout.unref && proc.stdout.unref();
|
package/lib/entrada.js
CHANGED
|
@@ -43,6 +43,10 @@ const { c, visibleWidth, truncate } = require('./ui.js');
|
|
|
43
43
|
|
|
44
44
|
// ── O desenho ────────────────────────────────────────────────────────────
|
|
45
45
|
|
|
46
|
+
/** A etiqueta de colagem em destaque (fininha, na cor da marca), sem mudar a largura. */
|
|
47
|
+
const RE_ETIQUETA = /\[Colado #\d+ · [^\]]+\]/g;
|
|
48
|
+
function pintarEtiquetas(s) { return String(s).replace(RE_ETIQUETA, (m) => c.brand(m) + '\x1b[38;5;254m'); }
|
|
49
|
+
|
|
46
50
|
const BORDA = { ne: '╭', no: '╮', se: '╰', so: '╯', h: '─', v: '│' };
|
|
47
51
|
|
|
48
52
|
/** Preenche até `w` colunas VISÍVEIS (códigos de cor não ocupam coluna). */
|
|
@@ -128,6 +132,12 @@ function montar(e) {
|
|
|
128
132
|
|
|
129
133
|
const out = [];
|
|
130
134
|
const regua = (bash ? c.warn : c.dim)(BORDA.h.repeat(largura));
|
|
135
|
+
/* O SPINNER MORA AQUI, na linha logo acima da caixa. Ele escrevia direto
|
|
136
|
+
no terminal com "\r" a cada 80 ms, sem quebra de linha — e a caixa,
|
|
137
|
+
que espera a linha fechar para se redesenhar, ficava apagada enquanto
|
|
138
|
+
ele girasse: o que se digitava não aparecia, e a barra de status
|
|
139
|
+
sumia ("não consigo digitar enquanto ele trabalha"). */
|
|
140
|
+
if (e.spinner) out.push(' ' + truncate(e.spinner, largura - 2));
|
|
131
141
|
out.push(regua);
|
|
132
142
|
|
|
133
143
|
const vazio = !String(e.texto).length;
|
|
@@ -137,13 +147,14 @@ function montar(e) {
|
|
|
137
147
|
: c.dim('┆ ');
|
|
138
148
|
// O "!" some do texto desenhado: quem vê "$ " já sabe que é shell, e
|
|
139
149
|
// mostrar os dois é mostrar a mesma informação duas vezes.
|
|
140
|
-
let conteudo = c.white(i === 0 && bash ? l.texto.replace(/^\s*!/, '') : l.texto);
|
|
150
|
+
let conteudo = pintarEtiquetas(c.white(i === 0 && bash ? l.texto.replace(/^\s*!/, '') : l.texto));
|
|
141
151
|
// O fantasma só existe na última linha e só com o cursor no fim dela:
|
|
142
152
|
// desenhado no meio do texto, ele empurraria o que vem depois.
|
|
143
153
|
if (i === linhas.length - 1 && e.fantasma && cursor === String(e.texto).length) {
|
|
144
154
|
conteudo += c.dim(e.fantasma);
|
|
145
155
|
}
|
|
146
|
-
if (i === 0 && vazio && e.
|
|
156
|
+
if (i === 0 && vazio && e.ocupado) conteudo = c.dim('mande um recado — ele entra na tarefa depois do passo atual');
|
|
157
|
+
else if (i === 0 && vazio && e.vazio) conteudo = c.dim(e.vazio);
|
|
147
158
|
// Sem `encher`: sem barra à direita não há nada para alinhar, e o
|
|
148
159
|
// espaço sobrando só apagaria o que estivesse embaixo.
|
|
149
160
|
out.push(' ' + marca + conteudo);
|
|
@@ -315,7 +326,8 @@ class Entrada extends EventEmitter {
|
|
|
315
326
|
suspender() { this.suspenso = true; this._apagar(); }
|
|
316
327
|
retomar() { this.suspenso = false; if (this.ativo && !this.fechado) this._desenhar(); }
|
|
317
328
|
|
|
318
|
-
/** O agente está trabalhando: a caixa esmaece e o Enter
|
|
329
|
+
/** O agente está trabalhando: a caixa esmaece, e o Enter manda um RECADO
|
|
330
|
+
* que entra na tarefa em curso (a caixa de entrada de lib/act.js). */
|
|
319
331
|
trabalhando(sim) {
|
|
320
332
|
if (this.ocupado === !!sim) return;
|
|
321
333
|
this.ocupado = !!sim;
|
|
@@ -324,6 +336,15 @@ class Entrada extends EventEmitter {
|
|
|
324
336
|
|
|
325
337
|
prompt() { if (!this.suspenso && this.tty && !this.fechado) this._desenhar(); }
|
|
326
338
|
|
|
339
|
+
/** O spinner (lib/ui.js) desenha por aqui quando há caixa: uma linha
|
|
340
|
+
* acima dela, redesenhada junto — nunca um "\r" solto por cima. */
|
|
341
|
+
mostrarSpinner(texto) {
|
|
342
|
+
const novo = texto || '';
|
|
343
|
+
if (novo === (this.spinnerTexto || '')) return;
|
|
344
|
+
this.spinnerTexto = novo;
|
|
345
|
+
if (this.desenhado && !this._meiaLinha) this._redesenhar();
|
|
346
|
+
}
|
|
347
|
+
|
|
327
348
|
/** Escreve texto ACIMA da caixa — é por onde todo o resto do programa fala. */
|
|
328
349
|
escreverAcima(texto) {
|
|
329
350
|
if (!this.desenhado) { this.escreverCru(texto); return; }
|
|
@@ -340,7 +361,40 @@ class Entrada extends EventEmitter {
|
|
|
340
361
|
this._mudou();
|
|
341
362
|
}
|
|
342
363
|
|
|
343
|
-
limpar() { this.line = ''; this.cursor = 0; this.menu = null; this._mudou(); }
|
|
364
|
+
limpar() { this.line = ''; this.cursor = 0; this.menu = null; this.colados = new Map(); this._mudou(); }
|
|
365
|
+
|
|
366
|
+
/* COLAR VIRA ETIQUETA, como no Claude Code: texto grande colado (várias
|
|
367
|
+
linhas, ou mais de 300 caracteres) entra na caixa como
|
|
368
|
+
"[Colado #1 · 42 linhas]" — a caixa não vira um paredão, dá para
|
|
369
|
+
escrever o pedido em volta, e no Enter a etiqueta volta a ser o texto
|
|
370
|
+
inteiro. O que é curto entra como texto mesmo. */
|
|
371
|
+
colar(texto) {
|
|
372
|
+
const t = String(texto || '');
|
|
373
|
+
if (!t) return;
|
|
374
|
+
const linhas = t.split('\n').length;
|
|
375
|
+
if (linhas < 3 && t.length <= 300) return this.write(t);
|
|
376
|
+
if (!this.colados) this.colados = new Map();
|
|
377
|
+
const n = this.colados.size + 1;
|
|
378
|
+
const etiqueta = `[Colado #${n} · ${linhas > 1 ? `${linhas} linhas` : `${t.length} caracteres`}]`;
|
|
379
|
+
this.colados.set(etiqueta, t);
|
|
380
|
+
this.write(etiqueta);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** A linha com cada etiqueta trocada pelo texto que ela guarda. */
|
|
384
|
+
expandir(linha) {
|
|
385
|
+
if (!this.colados || !this.colados.size) return linha;
|
|
386
|
+
let out = linha;
|
|
387
|
+
for (const [et, t] of this.colados) out = out.split(et).join(t);
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** A etiqueta que termina exatamente no cursor (o backspace apaga ela inteira). */
|
|
392
|
+
_etiquetaAntes() {
|
|
393
|
+
if (!this.colados) return null;
|
|
394
|
+
const antes = this.line.slice(0, this.cursor);
|
|
395
|
+
for (const et of this.colados.keys()) if (antes.endsWith(et)) return et;
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
344
398
|
|
|
345
399
|
/**
|
|
346
400
|
* A PRÓXIMA linha é senha: a caixa desenha `•` no lugar das letras, não
|
|
@@ -361,6 +415,7 @@ class Entrada extends EventEmitter {
|
|
|
361
415
|
status: this.op.status ? this.op.status() : [],
|
|
362
416
|
menu: this.menu,
|
|
363
417
|
ocupado: this.ocupado,
|
|
418
|
+
spinner: this.spinnerTexto || '',
|
|
364
419
|
});
|
|
365
420
|
}
|
|
366
421
|
|
|
@@ -434,11 +489,11 @@ class Entrada extends EventEmitter {
|
|
|
434
489
|
const seq = (key && key.sequence) || ch || '';
|
|
435
490
|
|
|
436
491
|
// Colagem entre marcas: tudo que vier até a marca de fim é texto.
|
|
437
|
-
if (seq.includes('\x1b[200~')) { this.colando = true; }
|
|
492
|
+
if (seq.includes('\x1b[200~')) { this.colando = true; this.bufColagem = ''; }
|
|
438
493
|
if (this.colando) {
|
|
439
494
|
const limpo = seq.replace('\x1b[200~', '').replace('\x1b[201~', '');
|
|
440
|
-
if (limpo) this.
|
|
441
|
-
if (seq.includes('\x1b[201~')) this.colando = false;
|
|
495
|
+
if (limpo) this.bufColagem += limpo.replace(/\r\n?/g, '\n');
|
|
496
|
+
if (seq.includes('\x1b[201~')) { this.colando = false; this.colar(this.bufColagem); this.bufColagem = ''; }
|
|
442
497
|
return;
|
|
443
498
|
}
|
|
444
499
|
|
|
@@ -476,7 +531,15 @@ class Entrada extends EventEmitter {
|
|
|
476
531
|
return;
|
|
477
532
|
}
|
|
478
533
|
|
|
479
|
-
case 'backspace':
|
|
534
|
+
case 'backspace': {
|
|
535
|
+
const et = this._etiquetaAntes();
|
|
536
|
+
if (et) {
|
|
537
|
+
this.line = this.line.slice(0, this.cursor - et.length) + this.line.slice(this.cursor);
|
|
538
|
+
this.cursor -= et.length;
|
|
539
|
+
this.colados.delete(et);
|
|
540
|
+
return this._mudou();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
480
543
|
if (this.cursor > 0) {
|
|
481
544
|
this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor);
|
|
482
545
|
this.cursor--;
|
|
@@ -583,7 +646,9 @@ class Entrada extends EventEmitter {
|
|
|
583
646
|
|
|
584
647
|
_enviar() {
|
|
585
648
|
if (this.menu) return this._escolherDoMenu();
|
|
586
|
-
const
|
|
649
|
+
const visto = this.line;
|
|
650
|
+
const texto = this.expandir(visto);
|
|
651
|
+
this.colados = new Map();
|
|
587
652
|
const oculto = this.oculto;
|
|
588
653
|
this.oculto = false;
|
|
589
654
|
this.line = '';
|
|
@@ -597,7 +662,8 @@ class Entrada extends EventEmitter {
|
|
|
597
662
|
this._apagar();
|
|
598
663
|
if (texto.trim() && !oculto) {
|
|
599
664
|
const marca = c.brand('❯ ');
|
|
600
|
-
|
|
665
|
+
// Na tela fica a etiqueta (o que se viu ao mandar); quem vai ao agente é o texto inteiro.
|
|
666
|
+
this.escreverCru(visto.split('\n').map((l, i) => (i ? ' ' : marca) + pintarEtiquetas(c.white(l))).join('\n') + '\n');
|
|
601
667
|
if (this.historico[this.historico.length - 1] !== texto) this.historico.push(texto);
|
|
602
668
|
}
|
|
603
669
|
this._desenhar();
|
|
@@ -607,4 +673,4 @@ class Entrada extends EventEmitter {
|
|
|
607
673
|
|
|
608
674
|
function criar(op) { return new Entrada(op).iniciar(); }
|
|
609
675
|
|
|
610
|
-
module.exports = { criar, Entrada, montar, quebrar, encher };
|
|
676
|
+
module.exports = { criar, Entrada, montar, quebrar, encher, pintarEtiquetas };
|