primocode 7.14.0 → 7.14.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/bin/primocode.js +32 -0
- package/lib/streaming.js +40 -19
- package/package.json +1 -1
package/bin/primocode.js
CHANGED
|
@@ -758,6 +758,38 @@ async function main() {
|
|
|
758
758
|
let closed = false;
|
|
759
759
|
let saidBye = false;
|
|
760
760
|
|
|
761
|
+
// ── Sugestão fantasma: o autopreenchimento VISÍVEL enquanto digita ──
|
|
762
|
+
// "/str" mostra "eaming" esmaecido na frente do cursor, como o Claude
|
|
763
|
+
// Code. Seta → (ou Tab) aceita; Enter com o pedaço já executa. Desenhada
|
|
764
|
+
// DEPOIS de o readline processar a tecla (setImmediate), por cima do que
|
|
765
|
+
// sobrou da linha (\\x1b[0K limpa), e o cursor volta para o lugar.
|
|
766
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
767
|
+
let fantasma = '';
|
|
768
|
+
const desenharFantasma = () => {
|
|
769
|
+
if (busy || pendingAsk || !rl) return;
|
|
770
|
+
const linha = rl.line;
|
|
771
|
+
const noFim = rl.cursor === linha.length;
|
|
772
|
+
fantasma = '';
|
|
773
|
+
if (noFim && linha.startsWith('/') && !/\s/.test(linha) && linha.length > 1) {
|
|
774
|
+
const baixa = linha.toLowerCase();
|
|
775
|
+
const hit = SLASH_COMANDOS.map(([n]) => n).find((n) => n.startsWith(baixa) && n.length > linha.length);
|
|
776
|
+
if (hit) fantasma = hit.slice(linha.length);
|
|
777
|
+
}
|
|
778
|
+
if (!noFim) return; // cursor no meio: não mexe no que vem depois dele
|
|
779
|
+
// Limpa resto da linha (fantasma antigo) e desenha o novo, voltando o cursor.
|
|
780
|
+
process.stdout.write('\x1b[0K' + (fantasma ? `\x1b[2m${fantasma}\x1b[0m\x1b[${fantasma.length}D` : ''));
|
|
781
|
+
};
|
|
782
|
+
process.stdin.on('keypress', (ch, key) => {
|
|
783
|
+
// Seta → no fim da linha aceita a sugestão inteira.
|
|
784
|
+
if (key && key.name === 'right' && fantasma && rl && rl.cursor === rl.line.length) {
|
|
785
|
+
const resto = fantasma;
|
|
786
|
+
fantasma = '';
|
|
787
|
+
rl.write(resto);
|
|
788
|
+
}
|
|
789
|
+
setImmediate(desenharFantasma);
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
761
793
|
function sayBye() {
|
|
762
794
|
if (saidBye) return;
|
|
763
795
|
saidBye = true;
|
package/lib/streaming.js
CHANGED
|
@@ -50,6 +50,8 @@ const state = {
|
|
|
50
50
|
liveAt: 0, // quando o último frame de vídeo foi capturado
|
|
51
51
|
viewerAt: 0, // último sinal de que alguém está com a página aberta
|
|
52
52
|
liveTimer: null,
|
|
53
|
+
liveBusy: false, // uma captura em voo por vez — sem isso cada poll da página
|
|
54
|
+
// abria OUTRA cadeia de captura e elas se multiplicavam
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
const LIVE = () => path.join(DIR, 'live.png');
|
|
@@ -69,25 +71,30 @@ function viuAgora() {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
function agendarVideo(ms) {
|
|
72
|
-
if (!state.on || state.liveTimer) return;
|
|
74
|
+
if (!state.on || state.liveTimer || state.liveBusy) return;
|
|
73
75
|
state.liveTimer = setTimeout(cicloVideo, ms);
|
|
74
76
|
state.liveTimer.unref();
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
function cicloVideo() {
|
|
78
80
|
state.liveTimer = null;
|
|
79
|
-
if (!state.on) return;
|
|
81
|
+
if (!state.on || state.liveBusy) return;
|
|
80
82
|
if (Date.now() - state.viewerAt > 8000) return; // ninguém olhando — o próximo /state religa
|
|
81
83
|
|
|
82
84
|
const cmd = desktop.screenshotCommand(LIVE_TMP());
|
|
83
85
|
if (!cmd) { if (!state.erroFoto) state.erroFoto = 'screenshot indisponível nesta máquina'; return; }
|
|
84
86
|
|
|
85
87
|
const t0 = Date.now();
|
|
88
|
+
state.liveBusy = true;
|
|
86
89
|
exec(cmd, { timeout: 8000 }, (err) => {
|
|
90
|
+
state.liveBusy = false;
|
|
87
91
|
const dur = Date.now() - t0;
|
|
88
92
|
if (!err) {
|
|
89
93
|
try {
|
|
90
|
-
|
|
94
|
+
// Rename é atômico; no Windows ele pode falhar se o navegador
|
|
95
|
+
// estiver lendo o arquivo NAQUELE instante — aí copia por cima.
|
|
96
|
+
try { fs.renameSync(LIVE_TMP(), LIVE()); }
|
|
97
|
+
catch { fs.copyFileSync(LIVE_TMP(), LIVE()); try { fs.unlinkSync(LIVE_TMP()); } catch {} }
|
|
91
98
|
state.liveSeq++;
|
|
92
99
|
state.liveAt = Date.now();
|
|
93
100
|
state.erroFoto = null;
|
|
@@ -424,25 +431,35 @@ let ultimoEstado = '';
|
|
|
424
431
|
|
|
425
432
|
function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
426
433
|
|
|
427
|
-
// ── o vídeo:
|
|
428
|
-
|
|
434
|
+
// ── o vídeo: busca o quadro direto, desde o primeiro segundo ──
|
|
435
|
+
// Não espera o /state confirmar nada: enquanto não há seq, tenta com
|
|
436
|
+
// timestamp (um 404 só significa "primeiro quadro ainda não saiu" — tenta de
|
|
437
|
+
// novo). Assim a tela aparece assim que o painel abre, sem depender de o
|
|
438
|
+
// agente ter feito qualquer coisa.
|
|
439
|
+
let quadroNaTela = '';
|
|
440
|
+
let buscandoQuadro = false;
|
|
429
441
|
function modoVideo() { return abaSel === VIVO && fotoSel == null; }
|
|
430
442
|
function videoTick() {
|
|
431
|
-
if (modoVideo() &&
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
443
|
+
if (modoVideo() && !buscandoQuadro) {
|
|
444
|
+
const marca = dados.liveSeq > 0 ? 's' + dados.liveSeq : 't' + Date.now();
|
|
445
|
+
if (marca !== quadroNaTela) {
|
|
446
|
+
buscandoQuadro = true;
|
|
447
|
+
const pre = new Image(); // pré-carrega fora da tela:
|
|
448
|
+
pre.onload = () => { // o quadro só entra inteiro
|
|
449
|
+
buscandoQuadro = false;
|
|
450
|
+
if (!modoVideo()) return;
|
|
451
|
+
const img = document.getElementById('frame');
|
|
452
|
+
img.src = pre.src; img.style.display = 'block';
|
|
453
|
+
document.getElementById('aviso').style.display = 'none';
|
|
454
|
+
document.getElementById('badge').style.display = 'flex';
|
|
455
|
+
document.getElementById('carimbo').style.display = 'none';
|
|
456
|
+
quadroNaTela = marca;
|
|
457
|
+
};
|
|
458
|
+
pre.onerror = () => { buscandoQuadro = false; };
|
|
459
|
+
pre.src = '/live?' + marca;
|
|
460
|
+
}
|
|
444
461
|
}
|
|
445
|
-
setTimeout(videoTick,
|
|
462
|
+
setTimeout(videoTick, 300);
|
|
446
463
|
}
|
|
447
464
|
|
|
448
465
|
// ── a instrução autopreenchendo, letra por letra ──
|
|
@@ -616,6 +633,10 @@ function start() {
|
|
|
616
633
|
state.server = server;
|
|
617
634
|
state.on = true;
|
|
618
635
|
state.url = `http://127.0.0.1:${server.address().port}`;
|
|
636
|
+
// O vídeo começa JÁ: quando o navegador terminar de abrir, o
|
|
637
|
+
// primeiro quadro está no ar — nada de tela vazia esperando ação.
|
|
638
|
+
state.viewerAt = Date.now();
|
|
639
|
+
agendarVideo(50);
|
|
619
640
|
resolve(state.url);
|
|
620
641
|
});
|
|
621
642
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.14.
|
|
3
|
+
"version": "7.14.1",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|