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 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
- for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
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
- fs.renameSync(LIVE_TMP(), LIVE());
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: 26px; width: 100px; height: 42px; transform: rotate(-32deg); filter: drop-shadow(0 8px 20px rgba(34,211,238,.4)); animation: flutua 3.2s ease-in-out infinite alternate; }
304
- @keyframes flutua { from { translate: 0 0 } to { translate: 0 5px } }
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
426
441
 
427
- // ── o vídeo: troca o quadro assim que um novo existe, sem piscar ──
428
- let quadroNaTela = -1;
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() && dados.liveSeq > 0 && dados.liveSeq !== quadroNaTela) {
432
- const seq = dados.liveSeq;
433
- const pre = new Image(); // pré-carrega fora da tela:
434
- pre.onload = () => { // o quadro só entra inteiro
435
- if (!modoVideo()) return;
436
- const img = document.getElementById('frame');
437
- img.src = pre.src; img.style.display = 'block';
438
- document.getElementById('aviso').style.display = 'none';
439
- document.getElementById('badge').style.display = 'flex';
440
- document.getElementById('carimbo').style.display = 'none';
441
- quadroNaTela = seq;
442
- };
443
- pre.src = '/live?' + seq;
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, 280);
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.14.0",
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": {