primocode 7.14.0 → 7.15.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/bin/primocode.js +51 -1
- package/lib/act.js +19 -2
- package/lib/streaming.js +53 -24
- package/package.json +1 -1
package/bin/primocode.js
CHANGED
|
@@ -19,7 +19,7 @@ const { escolherModo } = require('../lib/modo.js');
|
|
|
19
19
|
const api = require('../lib/api');
|
|
20
20
|
const tools = require('../lib/tools');
|
|
21
21
|
const cfg = require('../lib/config');
|
|
22
|
-
const { runAct } = require('../lib/act');
|
|
22
|
+
const { runAct, interromper } = require('../lib/act');
|
|
23
23
|
const pasta = require('../lib/pasta');
|
|
24
24
|
const catalogo = require('../lib/catalogo');
|
|
25
25
|
const streaming = require('../lib/streaming');
|
|
@@ -758,6 +758,56 @@ 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
|
+
// ── Esc Esc: parar o agente no meio do trabalho ──
|
|
783
|
+
// Um Esc avisa; o segundo (em até 2s) manda parar. Dois Escs tão
|
|
784
|
+
// rápidos que chegam colados num chunk só viram UMA tecla com
|
|
785
|
+
// sequence "\x1b\x1b" — vale como os dois.
|
|
786
|
+
let ultimoEsc = 0;
|
|
787
|
+
process.stdin.on('keypress', (ch, key) => {
|
|
788
|
+
if (key && (key.name === 'escape' || key.sequence === '\x1b\x1b')) {
|
|
789
|
+
if (!busy) { ultimoEsc = 0; return; } // sem agente rodando, Esc é só Esc
|
|
790
|
+
const agora = Date.now();
|
|
791
|
+
if (key.sequence === '\x1b\x1b' || agora - ultimoEsc < 2000) {
|
|
792
|
+
ultimoEsc = 0;
|
|
793
|
+
interromper();
|
|
794
|
+
process.stdout.write('\n' + c.warn(' ⏹ parando o agente…') + '\n');
|
|
795
|
+
} else {
|
|
796
|
+
ultimoEsc = agora;
|
|
797
|
+
process.stdout.write('\n' + c.dim(' Esc de novo para parar o agente') + '\n');
|
|
798
|
+
}
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
// Seta → no fim da linha aceita a sugestão inteira.
|
|
802
|
+
if (key && key.name === 'right' && fantasma && rl && rl.cursor === rl.line.length) {
|
|
803
|
+
const resto = fantasma;
|
|
804
|
+
fantasma = '';
|
|
805
|
+
rl.write(resto);
|
|
806
|
+
}
|
|
807
|
+
setImmediate(desenharFantasma);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
761
811
|
function sayBye() {
|
|
762
812
|
if (saidBye) return;
|
|
763
813
|
saidBye = true;
|
package/lib/act.js
CHANGED
|
@@ -23,6 +23,14 @@ const painel = require('./streaming');
|
|
|
23
23
|
const MAX_ITERATIONS = 15;
|
|
24
24
|
const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
|
|
25
25
|
|
|
26
|
+
// ── Interrupção (Esc Esc) ─────────────────────────────────────────────────
|
|
27
|
+
// O usuário aperta Esc duas vezes e o agente PARA — entre uma rodada e outra
|
|
28
|
+
// e entre uma tool e a seguinte (uma tool já em execução termina; o que ela
|
|
29
|
+
// fez, ficou feito). A flag é do módulo de propósito: parar o principal para
|
|
30
|
+
// os subagentes junto.
|
|
31
|
+
let PARAR = false;
|
|
32
|
+
function interromper() { PARAR = true; }
|
|
33
|
+
|
|
26
34
|
function ident(profundidade) {
|
|
27
35
|
return ' '.repeat(profundidade);
|
|
28
36
|
}
|
|
@@ -190,7 +198,10 @@ async function runAct(userPrompt, opts) {
|
|
|
190
198
|
// instrução de gente.
|
|
191
199
|
if (!subagent) { try { painel.instrucao(userPrompt); } catch {} }
|
|
192
200
|
|
|
193
|
-
|
|
201
|
+
// Pedido novo zera o pedido de parada anterior.
|
|
202
|
+
if (!subagent) PARAR = false;
|
|
203
|
+
|
|
204
|
+
for (let iteration = 0; iteration < MAX_ITERATIONS && !PARAR; iteration++) {
|
|
194
205
|
const sp = new Spinner(subagent ? 'subagente trabalhando' : 'Primo Code pensando').start();
|
|
195
206
|
|
|
196
207
|
const toolCalls = [];
|
|
@@ -251,6 +262,7 @@ async function runAct(userPrompt, opts) {
|
|
|
251
262
|
|
|
252
263
|
let finished = null;
|
|
253
264
|
for (const tc of toolCalls) {
|
|
265
|
+
if (PARAR) break;
|
|
254
266
|
if (tc.name === 'finish') {
|
|
255
267
|
// Encerrar com falha pendente é como o agente dizia "pronto!"
|
|
256
268
|
// sem ter mudado nada. Devolvemos a tarefa para ele.
|
|
@@ -306,6 +318,11 @@ async function runAct(userPrompt, opts) {
|
|
|
306
318
|
}
|
|
307
319
|
}
|
|
308
320
|
|
|
321
|
+
if (PARAR && !finalText) {
|
|
322
|
+
finalText = 'Parado por você (Esc Esc). O que já foi feito, ficou feito.';
|
|
323
|
+
if (!subagent) console.log('\n' + c.warn('⏹ ' + finalText));
|
|
324
|
+
}
|
|
325
|
+
|
|
309
326
|
// Nunca terminar em silêncio. Quando o modelo devolvia resposta vazia sem
|
|
310
327
|
// chamar tool, o usuário via a tela parar e mais nada — pior que erro, porque
|
|
311
328
|
// não dá nem para saber que acabou. Medido: um pedido de navegador morria assim
|
|
@@ -353,4 +370,4 @@ async function runSubagent(args, ctx) {
|
|
|
353
370
|
return resumo;
|
|
354
371
|
}
|
|
355
372
|
|
|
356
|
-
module.exports = { runAct };
|
|
373
|
+
module.exports = { runAct, interromper };
|
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;
|
|
@@ -267,10 +274,11 @@ function capturar(entrada) {
|
|
|
267
274
|
|
|
268
275
|
// O mascote do CLI (▐▛███▜▌ …) redesenhado em SVG, célula a célula, na mesma
|
|
269
276
|
// paleta: bloco cheio na cor viva, contorno na apagada.
|
|
277
|
+
// Olhos e pés têm classe própria: a página anima (piscada e passinhos) por CSS.
|
|
270
278
|
const MASCOTE_SVG = `<svg viewBox="0 0 9 3.6" aria-hidden="true">
|
|
271
279
|
<g fill="#0891b2"><rect x="1" y="0" width="1" height="1"/><rect x="7" y="0" width="1" height="1"/><rect x="0" y="1" width="1" height="1" opacity=".7"/><rect x="8" y="1" width="1" height="1" opacity=".7"/><rect x="1" y="1" width="1" height="1"/><rect x="7" y="1" width="1" height="1"/></g>
|
|
272
|
-
<g fill="#22d3ee"><rect x="2" y="0" width="5" height="1" rx=".12"/><rect x="2" y="1" width="5" height="1"/><rect x="1.9" y="2.2" width=".9" height=".5" rx=".1"/><rect x="3.4" y="2.2" width=".9" height=".5" rx=".1"/><rect x="4.9" y="2.2" width=".9" height=".5" rx=".1"/><rect x="6.4" y="2.2" width=".9" height=".5" rx=".1"/></g>
|
|
273
|
-
<g fill="#0b1220"><rect x="3" y=".35" width=".8" height=".5" rx=".1"/><rect x="5.2" y=".35" width=".8" height=".5" rx=".1"/><rect x="3.9" y="1.25" width="1.2" height=".28" rx=".14"/></g>
|
|
280
|
+
<g fill="#22d3ee"><rect x="2" y="0" width="5" height="1" rx=".12"/><rect x="2" y="1" width="5" height="1"/><rect class="pe pe1" x="1.9" y="2.2" width=".9" height=".5" rx=".1"/><rect class="pe pe2" x="3.4" y="2.2" width=".9" height=".5" rx=".1"/><rect class="pe pe1" x="4.9" y="2.2" width=".9" height=".5" rx=".1"/><rect class="pe pe2" x="6.4" y="2.2" width=".9" height=".5" rx=".1"/></g>
|
|
281
|
+
<g fill="#0b1220"><rect class="olho" x="3" y=".35" width=".8" height=".5" rx=".1"/><rect class="olho" x="5.2" y=".35" width=".8" height=".5" rx=".1"/><rect x="3.9" y="1.25" width="1.2" height=".28" rx=".14"/></g>
|
|
274
282
|
</svg>`;
|
|
275
283
|
|
|
276
284
|
// A tela do painel, na identidade do app — a MESMA paleta do design system do
|
|
@@ -300,9 +308,16 @@ const PAGINA = `<!doctype html>
|
|
|
300
308
|
|
|
301
309
|
/* ── Cabeçalho: mascote inclinado + nome, como no desenho ── */
|
|
302
310
|
#topo { position: relative; width: min(1120px, 100%); padding: 44px 0 26px; text-align: center; }
|
|
303
|
-
#topo .mascote { position: absolute; left: 4px; top:
|
|
304
|
-
@keyframes flutua {
|
|
305
|
-
#topo .mascote svg { width: 100%; height: 100%; display: block; }
|
|
311
|
+
#topo .mascote { position: absolute; left: 4px; top: 24px; width: 104px; height: 44px; filter: drop-shadow(0 8px 20px rgba(34,211,238,.4)); animation: flutua 2.8s ease-in-out infinite; }
|
|
312
|
+
@keyframes flutua { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(6px) } }
|
|
313
|
+
#topo .mascote svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
314
|
+
/* vivo: pisca os olhos de vez em quando e dá passinhos com os pés */
|
|
315
|
+
.mascote .olho { transform-box: fill-box; transform-origin: center; animation: piscaOlho 3.6s infinite; }
|
|
316
|
+
@keyframes piscaOlho { 0%, 89%, 100% { transform: scaleY(1) } 93%, 95% { transform: scaleY(.12) } }
|
|
317
|
+
.mascote .pe { transform-box: fill-box; transform-origin: center; }
|
|
318
|
+
.mascote .pe1 { animation: passinho 1.4s ease-in-out infinite; }
|
|
319
|
+
.mascote .pe2 { animation: passinho 1.4s ease-in-out infinite; animation-delay: .7s; }
|
|
320
|
+
@keyframes passinho { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(-.14px) } }
|
|
306
321
|
#topo h1 { font-size: clamp(34px, 5.6vw, 54px); font-weight: 650; letter-spacing: .4px; color: #f1f5f9; text-shadow: 0 4px 28px rgba(34,211,238,.22); }
|
|
307
322
|
#topo h1 span { color: #22d3ee; }
|
|
308
323
|
#topo .vivo { position: absolute; right: 2px; top: 48px; display: inline-flex; align-items: center; gap: 8px; color: #94a3b8; font-size: 12.5px; background: rgba(15,23,42,.85); border: 1px solid #1e293b; border-radius: 999px; padding: 6px 14px; }
|
|
@@ -424,25 +439,35 @@ let ultimoEstado = '';
|
|
|
424
439
|
|
|
425
440
|
function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
426
441
|
|
|
427
|
-
// ── o vídeo:
|
|
428
|
-
|
|
442
|
+
// ── o vídeo: busca o quadro direto, desde o primeiro segundo ──
|
|
443
|
+
// Não espera o /state confirmar nada: enquanto não há seq, tenta com
|
|
444
|
+
// timestamp (um 404 só significa "primeiro quadro ainda não saiu" — tenta de
|
|
445
|
+
// novo). Assim a tela aparece assim que o painel abre, sem depender de o
|
|
446
|
+
// agente ter feito qualquer coisa.
|
|
447
|
+
let quadroNaTela = '';
|
|
448
|
+
let buscandoQuadro = false;
|
|
429
449
|
function modoVideo() { return abaSel === VIVO && fotoSel == null; }
|
|
430
450
|
function videoTick() {
|
|
431
|
-
if (modoVideo() &&
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
451
|
+
if (modoVideo() && !buscandoQuadro) {
|
|
452
|
+
const marca = dados.liveSeq > 0 ? 's' + dados.liveSeq : 't' + Date.now();
|
|
453
|
+
if (marca !== quadroNaTela) {
|
|
454
|
+
buscandoQuadro = true;
|
|
455
|
+
const pre = new Image(); // pré-carrega fora da tela:
|
|
456
|
+
pre.onload = () => { // o quadro só entra inteiro
|
|
457
|
+
buscandoQuadro = false;
|
|
458
|
+
if (!modoVideo()) return;
|
|
459
|
+
const img = document.getElementById('frame');
|
|
460
|
+
img.src = pre.src; img.style.display = 'block';
|
|
461
|
+
document.getElementById('aviso').style.display = 'none';
|
|
462
|
+
document.getElementById('badge').style.display = 'flex';
|
|
463
|
+
document.getElementById('carimbo').style.display = 'none';
|
|
464
|
+
quadroNaTela = marca;
|
|
465
|
+
};
|
|
466
|
+
pre.onerror = () => { buscandoQuadro = false; };
|
|
467
|
+
pre.src = '/live?' + marca;
|
|
468
|
+
}
|
|
444
469
|
}
|
|
445
|
-
setTimeout(videoTick,
|
|
470
|
+
setTimeout(videoTick, 300);
|
|
446
471
|
}
|
|
447
472
|
|
|
448
473
|
// ── a instrução autopreenchendo, letra por letra ──
|
|
@@ -616,6 +641,10 @@ function start() {
|
|
|
616
641
|
state.server = server;
|
|
617
642
|
state.on = true;
|
|
618
643
|
state.url = `http://127.0.0.1:${server.address().port}`;
|
|
644
|
+
// O vídeo começa JÁ: quando o navegador terminar de abrir, o
|
|
645
|
+
// primeiro quadro está no ar — nada de tela vazia esperando ação.
|
|
646
|
+
state.viewerAt = Date.now();
|
|
647
|
+
agendarVideo(50);
|
|
619
648
|
resolve(state.url);
|
|
620
649
|
});
|
|
621
650
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.15.0",
|
|
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": {
|