primocode 7.13.0 → 7.14.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
@@ -78,6 +78,53 @@ ${c.bold('Onde ele trabalha')}
78
78
  ${c.dim('Pasta de projeto nova só nasce quando ele vai mexer em arquivo.')}
79
79
  `;
80
80
 
81
+ // ── Autopreenchimento dos comandos (estilo Claude Code) ────────────────
82
+ // Metade do comando já serve: Tab completa, e um prefixo único executa
83
+ // direto — "/str" vira "/streaming" sozinho. A lista é só a dos nomes
84
+ // canônicos; os apelidos (/cd, /stream…) continuam valendo por extenso.
85
+ const SLASH_COMANDOS = [
86
+ ['/help', 'todos os comandos'],
87
+ ['/project', 'lista/cria projetos em ~/primocode'],
88
+ ['/dir', 'trabalhar numa pasta sua'],
89
+ ['/mode', 'primocode | chat | auto'],
90
+ ['/model', 'top | main | fast'],
91
+ ['/attach', 'anexa arquivo ao pedido'],
92
+ ['/detach', 'remove os anexos'],
93
+ ['/files', 'lista os anexos'],
94
+ ['/tudo', 'tudo que ele sabe fazer'],
95
+ ['/tree', 'árvore do projeto'],
96
+ ['/read', 'mostra um arquivo'],
97
+ ['/run', 'roda um comando'],
98
+ ['/open', 'abre no navegador'],
99
+ ['/streaming', 'painel ao vivo — vídeo dele trabalhando'],
100
+ ['/status', 'checa o servidor'],
101
+ ['/name', 'como ele te chama'],
102
+ ['/mascot', 'primo | fox | cat'],
103
+ ['/config', 'suas preferências'],
104
+ ['/clear', 'limpa a conversa'],
105
+ ['/cls', 'limpa a tela'],
106
+ ['/uninstall', 'remove o PrimoCode'],
107
+ ['/version', 'versão'],
108
+ ['/exit', 'sair'],
109
+ ];
110
+
111
+ // O completer do readline: Tab completa o que der. Só age na palavra do
112
+ // comando — depois do primeiro espaço o argumento é seu, não dele.
113
+ function completarSlash(line) {
114
+ if (!line.startsWith('/') || /\s/.test(line)) return [[], line];
115
+ const baixa = line.toLowerCase();
116
+ const acertos = SLASH_COMANDOS.map(([nome]) => nome).filter((nome) => nome.startsWith(baixa));
117
+ return [acertos, line];
118
+ }
119
+
120
+ function mostrarMenuSlash() {
121
+ console.log('');
122
+ for (const [nome, desc] of SLASH_COMANDOS) {
123
+ console.log(` ${c.brand(ui.padRight(nome, 12))}${c.muted(desc)}`);
124
+ }
125
+ console.log(c.dim('\n Tab completa · metade do comando já serve: /str vira /streaming'));
126
+ }
127
+
81
128
  let rl = null;
82
129
 
83
130
  function makePrompt() {
@@ -605,7 +652,26 @@ async function handleSlash(text) {
605
652
  // rl.close() em vez de process.exit(): sair na marra descarta o que
606
653
  // ainda estava no buffer de saída.
607
654
  case 'exit': case 'quit': if (rl) rl.close(); break;
608
- default: console.log(c.err(`Comando desconhecido: /${cmd}. Digite /help.`));
655
+ default: {
656
+ // "/" sozinho abre o menu; um prefixo único autopreenche e executa
657
+ // ("/str" → /streaming); prefixo ambíguo mostra as opções.
658
+ if (!cmd) { mostrarMenuSlash(); break; }
659
+ const casam = SLASH_COMANDOS.map(([nome]) => nome.slice(1)).filter((n) => n.startsWith(cmd.toLowerCase()));
660
+ if (casam.length === 1) {
661
+ console.log(c.dim(` → /${casam[0]}`));
662
+ await handleSlash('/' + casam[0] + (rest ? ' ' + rest : ''));
663
+ break;
664
+ }
665
+ if (casam.length > 1) {
666
+ console.log(c.muted(` /${cmd}… pode ser:`));
667
+ for (const n of casam) {
668
+ const [, desc] = SLASH_COMANDOS.find(([nome]) => nome === '/' + n);
669
+ console.log(` ${c.brand(ui.padRight('/' + n, 12))}${c.dim(desc)}`);
670
+ }
671
+ break;
672
+ }
673
+ console.log(c.err(`Comando desconhecido: /${cmd}. Digite / para ver o menu.`));
674
+ }
609
675
  }
610
676
  }
611
677
 
@@ -682,7 +748,7 @@ async function main() {
682
748
  userName: state.config.userName,
683
749
  pastaPadrao: state.workDirOrigem === 'padrao',
684
750
  }));
685
- rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
751
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt(), completer: completarSlash });
686
752
  rl.prompt();
687
753
 
688
754
  // Fila: o readline continua emitindo 'line' enquanto um pedido está sendo
package/lib/desktop.js CHANGED
@@ -659,25 +659,40 @@ function clickElement(nome, opts = {}) {
659
659
  }
660
660
 
661
661
  // ── Screenshot ────────────────────────────────────────
662
- function screenshot(saveTo) {
663
- const filename = saveTo || `desktop-${Date.now()}.png`;
664
- const done = (r) => (r.ok ? { ok: true, path: filename, bytes: sizeOf(filename) } : r);
662
+ // A ferramenta do Linux é resolvida UMA vez: o vídeo do /streaming chama isto
663
+ // várias vezes por segundo, e um `which` por frame seria desperdício.
664
+ let LINUX_SHOT;
665
+ function linuxShot() {
666
+ if (LINUX_SHOT === undefined) LINUX_SHOT = firstCmd(['scrot', 'maim', 'gnome-screenshot', 'spectacle', 'import']);
667
+ return LINUX_SHOT;
668
+ }
665
669
 
666
- if (PLATFORM === 'darwin') {
667
- return done(run(`screencapture -x "${filename}"`));
668
- }
670
+ /**
671
+ * O comando de shell que fotografa a tela para `filename` — ou null quando a
672
+ * plataforma não tem como. Existe separado do screenshot() para o painel do
673
+ * /streaming capturar em LOOP, assíncrono, sem travar o agente.
674
+ */
675
+ function screenshotCommand(filename) {
676
+ if (PLATFORM === 'darwin') return `screencapture -x "${filename}"`;
669
677
  if (PLATFORM === 'win32') {
670
678
  const ps = `Add-Type -AssemblyName System.Drawing; Add-Type -AssemblyName System.Windows.Forms; $b=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; $bmp=New-Object System.Drawing.Bitmap($b.Width,$b.Height); $g=[System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Location,[System.Drawing.Point]::Empty,$bmp.Size); $bmp.Save('${filename.replace(/'/g, "''")}'); $g.Dispose(); $bmp.Dispose()`;
671
- return done(pwshRun(ps));
679
+ return pwshCmd(ps);
672
680
  }
673
- // Linux: tenta as ferramentas comuns, na ordem.
674
- const scrot = firstCmd(['scrot', 'gnome-screenshot', 'spectacle', 'import', 'maim']);
675
- if (scrot === 'scrot') return done(run(`scrot -o "${filename}"`));
676
- if (scrot === 'maim') return done(run(`maim "${filename}"`));
677
- if (scrot === 'gnome-screenshot') return done(run(`gnome-screenshot -f "${filename}"`));
678
- if (scrot === 'spectacle') return done(run(`spectacle -b -n -o "${filename}"`));
679
- if (scrot === 'import') return done(run(`import -window root "${filename}"`)); // ImageMagick
680
- return { ok: false, error: 'Screenshot indisponível no Linux. Instale scrot, gnome-screenshot, spectacle, maim ou ImageMagick (import).' };
681
+ const tool = linuxShot();
682
+ if (tool === 'scrot') return `scrot -o "${filename}"`;
683
+ if (tool === 'maim') return `maim "${filename}"`;
684
+ if (tool === 'gnome-screenshot') return `gnome-screenshot -f "${filename}"`;
685
+ if (tool === 'spectacle') return `spectacle -b -n -o "${filename}"`;
686
+ if (tool === 'import') return `import -window root "${filename}"`; // ImageMagick
687
+ return null;
688
+ }
689
+
690
+ function screenshot(saveTo) {
691
+ const filename = saveTo || `desktop-${Date.now()}.png`;
692
+ const cmd = screenshotCommand(filename);
693
+ if (!cmd) return { ok: false, error: 'Screenshot indisponível no Linux. Instale scrot, maim, gnome-screenshot, spectacle ou ImageMagick (import).' };
694
+ const r = run(cmd);
695
+ return r.ok ? { ok: true, path: filename, bytes: sizeOf(filename) } : r;
681
696
  }
682
697
 
683
698
  function sizeOf(f) {
@@ -719,7 +734,7 @@ module.exports = {
719
734
  click, doubleClick, move, drag, scroll,
720
735
  type, key,
721
736
  openApp, listWindows, focusWindow, uiElements, clickElement,
722
- screenshot, getScreenSize,
737
+ screenshot, screenshotCommand, getScreenSize,
723
738
  TOOLS,
724
739
  // Puras, exportadas para o test/desktop.test.js provar a tradução de teclas
725
740
  // e a escolha de elemento sem depender do SO da máquina de teste.
package/lib/streaming.js CHANGED
@@ -19,6 +19,7 @@ const http = require('http');
19
19
  const fs = require('fs');
20
20
  const path = require('path');
21
21
  const os = require('os');
22
+ const { exec } = require('child_process');
22
23
  const desktop = require('./desktop');
23
24
 
24
25
  const DIR = path.join(os.homedir(), '.primocode', 'streaming');
@@ -44,8 +45,61 @@ const state = {
44
45
  erroFoto: null,
45
46
  ultimaFoto: 0,
46
47
  passo: 0,
48
+ // ── vídeo ao vivo ──
49
+ liveSeq: 0, // sobe a cada frame novo; a página troca a imagem quando muda
50
+ liveAt: 0, // quando o último frame de vídeo foi capturado
51
+ viewerAt: 0, // último sinal de que alguém está com a página aberta
52
+ liveTimer: null,
47
53
  };
48
54
 
55
+ const LIVE = () => path.join(DIR, 'live.png');
56
+ const LIVE_TMP = () => path.join(DIR, 'live-tmp.png');
57
+
58
+ // ── O vídeo: um loop de captura enquanto alguém assiste ──────────────────
59
+ // "Vídeo" aqui é o jeito honesto de fazer sem dependência: fotografar a tela
60
+ // em sequência (2 a 3 quadros por segundo, conforme a máquina aguenta) e a
61
+ // página trocar a imagem no ritmo — como um VNC simples. O loop é assíncrono
62
+ // (exec, nunca execSync) para não roubar tempo do agente, escreve num tmp e
63
+ // renomeia (troca atômica: o navegador nunca lê um PNG pela metade), e SÓ
64
+ // roda enquanto a página deu sinal de vida há menos de 8s — painel fechado
65
+ // não custa nada.
66
+ function viuAgora() {
67
+ state.viewerAt = Date.now();
68
+ agendarVideo(0);
69
+ }
70
+
71
+ function agendarVideo(ms) {
72
+ if (!state.on || state.liveTimer) return;
73
+ state.liveTimer = setTimeout(cicloVideo, ms);
74
+ state.liveTimer.unref();
75
+ }
76
+
77
+ function cicloVideo() {
78
+ state.liveTimer = null;
79
+ if (!state.on) return;
80
+ if (Date.now() - state.viewerAt > 8000) return; // ninguém olhando — o próximo /state religa
81
+
82
+ const cmd = desktop.screenshotCommand(LIVE_TMP());
83
+ if (!cmd) { if (!state.erroFoto) state.erroFoto = 'screenshot indisponível nesta máquina'; return; }
84
+
85
+ const t0 = Date.now();
86
+ exec(cmd, { timeout: 8000 }, (err) => {
87
+ const dur = Date.now() - t0;
88
+ if (!err) {
89
+ try {
90
+ fs.renameSync(LIVE_TMP(), LIVE());
91
+ state.liveSeq++;
92
+ state.liveAt = Date.now();
93
+ state.erroFoto = null;
94
+ } catch {}
95
+ } else if (!state.erroFoto) {
96
+ state.erroFoto = corta(String(err.message || err), 120);
97
+ }
98
+ // Máquina rápida ≈ 3 quadros/s; máquina lenta desacelera sozinha.
99
+ agendarVideo(Math.max(280, Math.min(1200, dur)));
100
+ });
101
+ }
102
+
49
103
  // Ações cujo efeito aparece NA TELA — são as que merecem uma foto nova.
50
104
  const ACOES_NA_TELA = new Set([
51
105
  'desktop_click', 'desktop_double_click', 'desktop_click_element',
@@ -179,6 +233,20 @@ function capturar(entrada) {
179
233
 
180
234
  const id = state.proximoFrame;
181
235
  const arquivo = path.join(DIR, `frame-${id}.png`);
236
+
237
+ // Com o vídeo rodando, o quadro mais novo já está no disco — copiar é
238
+ // grátis e não fotografa duas vezes a mesma tela.
239
+ if (agora - state.liveAt < 900) {
240
+ try { fs.copyFileSync(LIVE(), arquivo); } catch { return null; }
241
+ state.proximoFrame++;
242
+ state.frames.push({ id, hora: entrada.hora, acao: entrada.acao, aba: entrada.aba });
243
+ while (state.frames.length > MAX_FRAMES) {
244
+ const velho = state.frames.shift();
245
+ try { fs.unlinkSync(path.join(DIR, `frame-${velho.id}.png`)); } catch {}
246
+ }
247
+ return id;
248
+ }
249
+
182
250
  const r = desktop.screenshot(arquivo);
183
251
  if (!r.ok) { if (!state.erroFoto) state.erroFoto = r.error; return null; }
184
252
 
@@ -205,13 +273,13 @@ const MASCOTE_SVG = `<svg viewBox="0 0 9 3.6" aria-hidden="true">
205
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>
206
274
  </svg>`;
207
275
 
208
- // A tela finalizada, a partir do desenho do dono: fundo grafite, mascote
209
- // inclinado, "Primocode" branco no alto; a janela de borda branca grossa com
210
- // as três bolinhas e as abas separadas por "|"; e, novidade combinada, a
211
- // BARRA DE COMANDO — a instrução que você deu no terminal aparece ali,
212
- // autopreenchendo letra por letra, com cursor piscando, enquanto o agente
213
- // executa. Abaixo da moldura, a linha do tempo: miniaturas para voltar em
214
- // qualquer passo e a lista de ações, com cada pedido marcando onde começou.
276
+ // A tela do painel, na identidade do app — a MESMA paleta do design system do
277
+ // CLI (lib/ui.js): primária cyan #22d3ee, neutros slate, rose para falha. A
278
+ // estrutura é a do desenho do dono: mascote inclinado no canto, "Primocode"
279
+ // no alto, a janela com as bolinhas e as abas separadas por "|", a tela
280
+ // embaixo. Dentro da tela roda o VÍDEO ao vivo (o loop de captura acima); as
281
+ // abas em cima são tudo que o agente abriu — clicáveis, cada uma com seus
282
+ // quadros e passos; e a barra de comando autopreenche a instrução digitando.
215
283
  const FAVICON = 'data:image/svg+xml,' + encodeURIComponent(
216
284
  `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 3.6">${MASCOTE_SVG.replace(/^<svg[^>]*>|<\/svg>$/g, '')}</svg>`,
217
285
  );
@@ -223,94 +291,97 @@ const PAGINA = `<!doctype html>
223
291
  <link rel="icon" href="${FAVICON}">
224
292
  <style>
225
293
  * { box-sizing: border-box; margin: 0; }
226
- ::selection { background: rgba(255,255,255,.2); }
294
+ ::selection { background: rgba(34,211,238,.28); }
227
295
  body {
228
- background: radial-gradient(1200px 500px at 50% -100px, #2e2e2e 0%, #262626 55%, #1f1f1f 100%) fixed #262626;
229
- color: #fff; font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
296
+ background: radial-gradient(1100px 480px at 50% -120px, #12263f 0%, #0d1729 46%, #0b1220 100%) fixed #0b1220;
297
+ color: #e2e8f0; font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
230
298
  min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 0 18px 34px;
231
299
  }
232
300
 
233
- /* ── Cabeçalho ── */
234
- #topo { position: relative; width: min(1120px, 100%); padding: 46px 0 28px; text-align: center; }
235
- #topo .mascote { position: absolute; left: 4px; top: 28px; width: 100px; height: 42px; transform: rotate(-32deg); filter: drop-shadow(0 6px 14px rgba(34,211,238,.35)); animation: flutua 3.2s ease-in-out infinite alternate; }
301
+ /* ── Cabeçalho: mascote inclinado + nome, como no desenho ── */
302
+ #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; }
236
304
  @keyframes flutua { from { translate: 0 0 } to { translate: 0 5px } }
237
305
  #topo .mascote svg { width: 100%; height: 100%; display: block; }
238
- #topo h1 { font-size: clamp(36px, 6vw, 60px); font-weight: 650; letter-spacing: .5px; text-shadow: 0 3px 22px rgba(0,0,0,.55); }
239
- #topo .vivo { position: absolute; right: 2px; top: 50px; display: inline-flex; align-items: center; gap: 8px; color: #a9a9a9; font-size: 12.5px; background: #1c1c1cd9; border: 1px solid #363636; border-radius: 999px; padding: 6px 14px; }
240
- .dot { width: 9px; height: 9px; border-radius: 50%; background: #46d160; box-shadow: 0 0 8px #46d16088; animation: pulsa 1.4s infinite; }
306
+ #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
+ #topo h1 span { color: #22d3ee; }
308
+ #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; }
309
+ .pulso { width: 8px; height: 8px; border-radius: 50%; background: #22d3ee; box-shadow: 0 0 10px #22d3ee99; animation: pulsa 1.4s infinite; }
241
310
  @keyframes pulsa { 50% { opacity: .3 } }
242
311
 
243
312
  /* ── A janela ── */
244
313
  #janela {
245
- width: min(1120px, 100%); background: #000; border: 9px solid #f5f5f5; border-radius: 44px;
314
+ width: min(1120px, 100%); background: #000; border: 1px solid #24344d; border-radius: 22px;
246
315
  overflow: hidden; display: flex; flex-direction: column;
247
- box-shadow: 0 34px 90px rgba(0,0,0,.65), 0 0 0 1px rgba(255,255,255,.06), 0 10px 34px rgba(0,0,0,.5);
316
+ box-shadow: 0 30px 80px rgba(2,6,23,.75), 0 0 60px rgba(34,211,238,.07), inset 0 1px 0 rgba(226,232,240,.07);
248
317
  }
249
- #barra { position: relative; display: flex; align-items: center; background: linear-gradient(#262626, #212121); padding: 15px 28px; min-height: 60px; border-bottom: 1px solid #0c0c0c; }
250
- #barra .b { flex: none; margin-right: 12px; box-shadow: inset 0 -2px 4px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.5); }
251
- #barra .b.r { width: 17px; height: 17px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #ff5a5a, #d40000); }
252
- #barra .b.y { width: 30px; height: 17px; border-radius: 999px; background: radial-gradient(circle at 35% 30%, #ffe95c, #f5c400); }
253
- #barra .b.g { width: 24px; height: 14px; border-radius: 999px; background: radial-gradient(circle at 35% 30%, #6fe589, #24b53e); }
318
+ #barra { position: relative; display: flex; align-items: center; background: linear-gradient(#152238, #101a2c); padding: 13px 22px; min-height: 54px; border-bottom: 1px solid #0a1120; }
319
+ #barra .b { flex: none; width: 13px; height: 13px; border-radius: 50%; margin-right: 9px; box-shadow: inset 0 -1.5px 3px rgba(0,0,0,.35); }
320
+ #barra .b.r { background: #ff5f57; } #barra .b.y { background: #febc2e; } #barra .b.g { background: #28c840; }
254
321
 
255
- #abas { position: absolute; left: 50%; transform: translateX(-50%); max-width: 60%; display: flex; align-items: center; gap: 13px; overflow-x: auto; white-space: nowrap; scrollbar-width: none; font-size: clamp(15px, 2.2vw, 22px); font-weight: 550; }
322
+ /* Abas: tudo que ele abriu, clicável, separado por | como no desenho */
323
+ #abas { position: absolute; left: 50%; transform: translateX(-50%); max-width: 62%; display: flex; align-items: center; gap: 11px; overflow-x: auto; white-space: nowrap; scrollbar-width: none; font-size: clamp(14px, 1.9vw, 18px); font-weight: 550; }
256
324
  #abas::-webkit-scrollbar { display: none; }
257
- .aba { border: 0; background: none; cursor: pointer; font: inherit; color: rgba(255,255,255,.45); padding: 3px 2px; transition: color .18s ease, text-shadow .18s ease; }
258
- .aba:hover { color: rgba(255,255,255,.85); }
259
- .aba.ativa { color: #fff; text-shadow: 0 0 16px rgba(255,255,255,.45); }
260
- .aba .qtd { color: rgba(255,255,255,.35); font-size: .62em; vertical-align: super; margin-left: 4px; font-weight: 600; }
261
- .sep { color: rgba(255,255,255,.28); font-weight: 300; }
325
+ .aba { position: relative; border: 0; background: none; cursor: pointer; font: inherit; color: #64748b; padding: 4px 3px 7px; transition: color .18s ease; }
326
+ .aba:hover { color: #cbd5e1; }
327
+ .aba.ativa { color: #22d3ee; text-shadow: 0 0 18px rgba(34,211,238,.5); }
328
+ .aba.ativa::after { content: ''; position: absolute; left: 15%; right: 15%; bottom: 0; height: 2px; border-radius: 2px; background: #22d3ee; box-shadow: 0 0 10px #22d3ee; }
329
+ .aba .qtd { color: #475569; font-size: .68em; vertical-align: super; margin-left: 4px; font-weight: 600; }
330
+ .sep { color: #334155; font-weight: 300; }
262
331
 
263
332
  /* ── Barra de comando: a instrução autopreenchendo ── */
264
- #comando { display: flex; align-items: center; gap: 12px; background: #141414; border-bottom: 1px solid #0a0a0a; padding: 12px 26px; font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; font-size: 14.5px; min-height: 46px; }
265
- #comando .chevron { color: #46d160; font-weight: 700; flex: none; }
266
- #comando .texto { color: #eaeaea; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
267
- #comando .texto.vazio { color: #5c5c5c; font-style: italic; }
268
- #comando .cursor { flex: none; width: 8px; height: 17px; background: #eaeaea; margin-left: 2px; animation: pisca 1s steps(1) infinite; }
333
+ #comando { display: flex; align-items: center; gap: 11px; background: #0a1120; border-bottom: 1px solid #060b16; padding: 11px 22px; font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; font-size: 14px; min-height: 44px; }
334
+ #comando .chevron { color: #22d3ee; font-weight: 700; flex: none; text-shadow: 0 0 10px rgba(34,211,238,.6); }
335
+ #comando .texto { color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
336
+ #comando .texto.vazio { color: #475569; font-style: italic; }
337
+ #comando .cursor { flex: none; width: 8px; height: 16px; background: #22d3ee; margin-left: 2px; animation: pisca 1s steps(1) infinite; box-shadow: 0 0 8px rgba(34,211,238,.7); }
269
338
  @keyframes pisca { 50% { opacity: 0 } }
270
- #comando .hora { margin-left: auto; color: #5c5c5c; font-size: 11.5px; flex: none; }
271
-
272
- /* ── A tela ── */
273
- #tela { position: relative; min-height: 460px; display: flex; align-items: center; justify-content: center; background: #000; }
274
- #tela img { width: 100%; max-height: 62vh; object-fit: contain; display: none; opacity: 0; transition: opacity .28s ease; }
275
- #aviso { color: #fff; text-align: center; padding: 90px 24px; }
276
- #aviso .titulo { font-size: clamp(20px, 3.2vw, 32px); font-weight: 550; }
277
- #aviso .sub { color: #8f8f8f; font-size: 14.5px; margin-top: 14px; line-height: 1.7; }
278
- #carimbo { position: absolute; left: 18px; bottom: 14px; background: rgba(10,10,10,.72); backdrop-filter: blur(6px); border: 1px solid #2e2e2e; color: #c9c9c9; font-size: 12px; padding: 5px 13px; border-radius: 999px; display: none; max-width: 85%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
279
- #carimbo b { color: #fff; font-weight: 600; }
339
+ #comando .hora { margin-left: auto; color: #475569; font-size: 11.5px; flex: none; }
340
+
341
+ /* ── A tela: o vídeo ao vivo ── */
342
+ #tela { position: relative; min-height: 440px; display: flex; align-items: center; justify-content: center; background: #000; }
343
+ #tela img { width: 100%; max-height: 60vh; object-fit: contain; display: none; }
344
+ #aviso { text-align: center; padding: 84px 24px; }
345
+ #aviso .titulo { font-size: clamp(19px, 3vw, 28px); font-weight: 550; color: #f1f5f9; }
346
+ #aviso .sub { color: #64748b; font-size: 14px; margin-top: 13px; line-height: 1.7; }
347
+ #badge { position: absolute; top: 14px; right: 16px; display: none; align-items: center; gap: 7px; background: rgba(8,12,22,.78); backdrop-filter: blur(6px); border: 1px solid #1e293b; color: #f1f5f9; font-size: 11.5px; font-weight: 700; letter-spacing: 1.2px; padding: 6px 12px; border-radius: 999px; }
348
+ #badge .rec { width: 8px; height: 8px; border-radius: 50%; background: #fb7185; box-shadow: 0 0 9px #fb7185; animation: pulsa 1.1s infinite; }
349
+ #carimbo { position: absolute; left: 16px; bottom: 13px; background: rgba(8,12,22,.78); backdrop-filter: blur(6px); border: 1px solid #1e293b; color: #94a3b8; font-size: 12px; padding: 5px 13px; border-radius: 999px; display: none; max-width: 82%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
350
+ #carimbo b { color: #22d3ee; font-weight: 600; }
280
351
 
281
352
  /* ── Linha do tempo ── */
282
- #historico { width: min(1120px, 100%); padding-top: 20px; }
283
- #filme { display: flex; gap: 10px; padding: 4px 2px 14px; overflow-x: auto; scrollbar-width: thin; scrollbar-color: #3a3a3a transparent; }
284
- .foto { flex: none; border: 1.5px solid #3a3a3a; border-radius: 10px; background: #000; cursor: pointer; padding: 0; width: 104px; height: 62px; overflow: hidden; opacity: .65; transition: transform .16s ease, opacity .16s ease, border-color .16s ease, box-shadow .16s ease; }
285
- .foto:hover { opacity: 1; transform: translateY(-2px); border-color: #7a7a7a; }
353
+ #historico { width: min(1120px, 100%); padding-top: 18px; }
354
+ #filme { display: flex; gap: 9px; padding: 4px 2px 13px; overflow-x: auto; scrollbar-width: thin; scrollbar-color: #1e293b transparent; }
355
+ .foto { flex: none; border: 1.5px solid #1e293b; border-radius: 9px; background: #000; cursor: pointer; padding: 0; width: 100px; height: 60px; overflow: hidden; opacity: .6; transition: transform .16s ease, opacity .16s ease, border-color .16s ease, box-shadow .16s ease; }
356
+ .foto:hover { opacity: 1; transform: translateY(-2px); border-color: #475569; }
286
357
  .foto img { width: 100%; height: 100%; object-fit: cover; display: block; }
287
- .foto.ativa { border-color: #fff; opacity: 1; box-shadow: 0 0 0 2px rgba(255,255,255,.25), 0 6px 18px rgba(0,0,0,.45); }
288
-
289
- #cartao { background: #1c1c1c; border: 1px solid #2c2c2c; border-radius: 18px; overflow: hidden; }
290
- #cartao header { display: flex; align-items: center; justify-content: space-between; padding: 12px 18px; border-bottom: 1px solid #272727; color: #bdbdbd; font-size: 13px; font-weight: 600; letter-spacing: .3px; }
291
- #cartao header span b { color: #fff; }
292
- #passos { max-height: 240px; overflow-y: auto; padding: 6px 0 10px; scrollbar-width: thin; scrollbar-color: #3a3a3a transparent; }
293
- #passos::-webkit-scrollbar { width: 8px; } #passos::-webkit-scrollbar-thumb { background: #3a3a3a; border-radius: 4px; }
294
- .item { padding: 5px 18px; display: flex; gap: 10px; font-size: 13.5px; align-items: baseline; transition: background .12s ease; }
295
- .item:hover { background: #222; }
296
- .item .n { color: #6b6b6b; min-width: 3ch; text-align: right; font-variant-numeric: tabular-nums; }
297
- .item .ok { color: #46d160; } .item .falha { color: #ff6b6b; }
298
- .item .txt { flex: 1; word-break: break-word; color: #dcdcdc; }
299
- .item .txt .erro { color: #ff8a8a; font-size: 12px; display: block; margin-top: 1px; }
300
- .item .hora { color: #6b6b6b; font-size: 11px; font-variant-numeric: tabular-nums; }
301
- .item.temfoto { cursor: pointer; } .item.temfoto:hover .txt { color: #fff; }
302
- .item.pedido { background: #191919; border-top: 1px solid #262626; border-bottom: 1px solid #262626; margin: 6px 0; padding: 8px 18px; }
303
- .item.pedido .txt { color: #fff; font-weight: 600; }
304
- .item.pedido .chev { color: #46d160; font-weight: 800; }
305
-
306
- footer { color: #6b6b6b; font-size: 12px; padding-top: 18px; }
307
- footer b { color: #9a9a9a; font-weight: 600; }
358
+ .foto.ativa { border-color: #22d3ee; opacity: 1; box-shadow: 0 0 0 2px rgba(34,211,238,.3), 0 6px 18px rgba(2,6,23,.6); }
359
+
360
+ #cartao { background: #0f172a; border: 1px solid #1e293b; border-radius: 16px; overflow: hidden; }
361
+ #cartao header { display: flex; align-items: center; justify-content: space-between; padding: 11px 17px; border-bottom: 1px solid #16223a; color: #94a3b8; font-size: 13px; font-weight: 600; letter-spacing: .3px; }
362
+ #cartao header span b { color: #22d3ee; }
363
+ #passos { max-height: 236px; overflow-y: auto; padding: 5px 0 9px; scrollbar-width: thin; scrollbar-color: #1e293b transparent; }
364
+ #passos::-webkit-scrollbar { width: 8px; } #passos::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 4px; }
365
+ .item { padding: 5px 17px; display: flex; gap: 10px; font-size: 13.5px; align-items: baseline; transition: background .12s ease; }
366
+ .item:hover { background: #131f35; }
367
+ .item .n { color: #475569; min-width: 3ch; text-align: right; font-variant-numeric: tabular-nums; }
368
+ .item .ok { color: #22d3ee; } .item .falha { color: #fb7185; }
369
+ .item .txt { flex: 1; word-break: break-word; color: #cbd5e1; }
370
+ .item .txt .erro { color: #fb7185; font-size: 12px; display: block; margin-top: 1px; }
371
+ .item .hora { color: #475569; font-size: 11px; font-variant-numeric: tabular-nums; }
372
+ .item.temfoto { cursor: pointer; } .item.temfoto:hover .txt { color: #f1f5f9; }
373
+ .item.pedido { background: #0c1526; border-top: 1px solid #16223a; border-bottom: 1px solid #16223a; margin: 6px 0; padding: 8px 17px; }
374
+ .item.pedido .txt { color: #f1f5f9; font-weight: 600; }
375
+ .item.pedido .chev { color: #22d3ee; font-weight: 800; }
376
+
377
+ footer { color: #475569; font-size: 12px; padding-top: 17px; }
378
+ footer b { color: #64748b; font-weight: 600; }
308
379
  </style></head>
309
380
  <body>
310
381
  <div id="topo">
311
382
  <div class="mascote">${MASCOTE_SVG}</div>
312
- <h1>Primocode</h1>
313
- <span class="vivo"><span class="dot"></span><span id="st">conectando…</span></span>
383
+ <h1>Primo<span>code</span></h1>
384
+ <span class="vivo"><span class="pulso"></span><span id="st">conectando…</span></span>
314
385
  </div>
315
386
 
316
387
  <div id="janela">
@@ -325,11 +396,12 @@ const PAGINA = `<!doctype html>
325
396
  <span class="hora" id="cmdhora"></span>
326
397
  </div>
327
398
  <section id="tela">
328
- <img id="frame" alt="tela que a IA está usando">
399
+ <img id="frame" alt="tela do computador, ao vivo">
329
400
  <div id="aviso">
330
- <div class="titulo">Tela que a IA está usando aparece aqui</div>
331
- <div class="sub">Dê uma instrução no terminal do PrimoCode e veja cada passo, ao vivo.<br>Cada app em que ele mexer vira uma aba — volte nelas para rever o que já foi feito.</div>
401
+ <div class="titulo">A tela do computador aparece aqui, em vídeo</div>
402
+ <div class="sub">Dê uma instrução no terminal do PrimoCode e assista ele trabalhando, ao vivo.<br>Cada app que ele abrir vira uma aba lá em cima — clique nelas para rever o que já foi feito.</div>
332
403
  </div>
404
+ <div id="badge"><span class="rec"></span>AO VIVO</div>
333
405
  <div id="carimbo"></div>
334
406
  </section>
335
407
  </div>
@@ -345,12 +417,35 @@ const PAGINA = `<!doctype html>
345
417
 
346
418
  <script>
347
419
  const VIVO = 0; // id da aba "Ao vivo"
348
- let dados = { abas: [], frames: [], log: [], instrucoes: [] };
420
+ let dados = { abas: [], frames: [], log: [], instrucoes: [], liveSeq: 0 };
349
421
  let abaSel = VIVO; // aba escolhida pelo usuário
350
- let fotoSel = null; // frame fixado no filmstrip (null = mais recente)
422
+ let fotoSel = null; // frame fixado (null = vídeo/mais recente)
351
423
  let ultimoEstado = '';
352
424
 
353
- // ── autopreencher a instrução, letra por letra ──
425
+ function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
426
+
427
+ // ── o vídeo: troca o quadro assim que um novo existe, sem piscar ──
428
+ let quadroNaTela = -1;
429
+ function modoVideo() { return abaSel === VIVO && fotoSel == null; }
430
+ 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;
444
+ }
445
+ setTimeout(videoTick, 280);
446
+ }
447
+
448
+ // ── a instrução autopreenchendo, letra por letra ──
354
449
  let digitando = null, instrucaoNaTela = 0;
355
450
  function autopreencher(inst) {
356
451
  if (!inst || inst.n === instrucaoNaTela) return;
@@ -362,14 +457,12 @@ function autopreencher(inst) {
362
457
  document.getElementById('cmdhora').textContent = inst.hora;
363
458
  let i = 0;
364
459
  digitando = setInterval(() => {
365
- i += 1 + Math.floor(Math.random() * 2); // cadência de gente digitando
460
+ i += 1 + Math.floor(Math.random() * 2); // cadência de gente digitando
366
461
  el.textContent = inst.texto.slice(0, i);
367
462
  if (i >= inst.texto.length) clearInterval(digitando);
368
- }, 34);
463
+ }, 32);
369
464
  }
370
465
 
371
- function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
372
-
373
466
  function framesDaAba(id) {
374
467
  return id === VIVO ? dados.frames : dados.frames.filter((f) => f.aba === id);
375
468
  }
@@ -378,46 +471,52 @@ function logDaAba(id) {
378
471
  }
379
472
 
380
473
  function render() {
381
- // Abas: "Ao vivo" + uma por app/janela, separadas por |
474
+ // Abas: "Ao vivo" + tudo que ele abriu, separadas por |
382
475
  const nav = document.getElementById('abas');
383
476
  const abas = [{ id: VIVO, nome: 'Ao vivo' }, ...dados.abas];
384
477
  nav.innerHTML = abas.map((a) =>
385
478
  '<button class="aba' + (a.id === abaSel ? ' ativa' : '') + '" data-id="' + a.id + '">' + esc(a.nome)
386
479
  + (a.passos ? '<span class="qtd">' + a.passos + '</span>' : '') + '</button>'
387
480
  ).join('<span class="sep">|</span>');
388
- nav.querySelectorAll('.aba').forEach((b) => b.onclick = () => { abaSel = Number(b.dataset.id); fotoSel = null; render(); });
481
+ nav.querySelectorAll('.aba').forEach((b) => b.onclick = () => {
482
+ abaSel = Number(b.dataset.id); fotoSel = null; quadroNaTela = -1; render();
483
+ });
389
484
  const abaObj = abas.find((a) => a.id === abaSel) || abas[0];
390
485
  document.getElementById('abanome').textContent = abaObj.nome;
391
486
 
392
- // Imagem: a fixada no filmstrip, senão a mais nova da aba
487
+ // Fora do modo vídeo (aba antiga ou foto fixada): mostra o quadro daquela aba
393
488
  const fs = framesDaAba(abaSel);
394
- const atual = fotoSel != null ? fs.find((f) => f.id === fotoSel) : fs[fs.length - 1];
395
489
  const img = document.getElementById('frame');
396
490
  const aviso = document.getElementById('aviso');
491
+ const badge = document.getElementById('badge');
397
492
  const carimbo = document.getElementById('carimbo');
398
- if (atual) {
399
- if (img.dataset.n !== String(atual.id)) {
400
- img.style.opacity = '0';
401
- img.onload = () => { img.style.opacity = '1'; };
402
- img.src = '/frame?n=' + atual.id;
403
- img.dataset.n = String(atual.id);
493
+ if (!modoVideo()) {
494
+ const atual = fotoSel != null ? fs.find((f) => f.id === fotoSel) : fs[fs.length - 1];
495
+ badge.style.display = 'none';
496
+ if (atual) {
497
+ if (img.dataset.n !== 'f' + atual.id) { img.src = '/frame?n=' + atual.id; img.dataset.n = 'f' + atual.id; }
498
+ img.style.display = 'block'; aviso.style.display = 'none';
499
+ carimbo.style.display = 'block';
500
+ carimbo.innerHTML = '<b>#' + atual.id + '</b> · ' + atual.hora + ' · ' + esc(atual.acao)
501
+ + (fotoSel != null ? ' — <b>clique na miniatura de novo para voltar</b>' : '');
502
+ } else {
503
+ img.style.display = 'none'; aviso.style.display = 'block'; carimbo.style.display = 'none';
404
504
  }
405
- img.style.display = 'block'; aviso.style.display = 'none';
406
- carimbo.style.display = 'block';
407
- carimbo.innerHTML = '<b>#' + atual.id + '</b> · ' + atual.hora + ' · ' + esc(atual.acao) + (fotoSel != null ? ' — <b>clique na miniatura de novo para voltar ao ao-vivo</b>' : '');
408
505
  } else {
409
- img.style.display = 'none'; aviso.style.display = 'block'; carimbo.style.display = 'none';
410
- if (dados.erroFoto) aviso.querySelector('.sub').textContent = 'Sem foto da tela: ' + dados.erroFoto;
506
+ carimbo.style.display = 'none';
507
+ if (dados.erroFoto) { aviso.style.display = 'block'; aviso.querySelector('.sub').textContent = 'Sem imagem da tela: ' + dados.erroFoto; }
411
508
  }
412
509
 
413
510
  // Filmstrip da aba (as últimas 16)
414
511
  const filme = document.getElementById('filme');
415
512
  const tira = fs.slice(-16);
513
+ const marcada = fotoSel != null ? fotoSel : -1;
416
514
  filme.innerHTML = tira.map((f) =>
417
- '<button class="foto' + ((atual && f.id === atual.id) ? ' ativa' : '') + '" data-id="' + f.id + '" title="' + esc(f.acao) + '"><img loading="lazy" src="/frame?n=' + f.id + '"></button>').join('');
515
+ '<button class="foto' + (f.id === marcada ? ' ativa' : '') + '" data-id="' + f.id + '" title="' + esc(f.acao) + '"><img loading="lazy" src="/frame?n=' + f.id + '"></button>').join('');
418
516
  filme.querySelectorAll('.foto').forEach((b) => b.onclick = () => {
419
517
  const id = Number(b.dataset.id);
420
- fotoSel = (fotoSel === id) ? null : id; // clicar de novo volta ao "mais recente"
518
+ fotoSel = (fotoSel === id) ? null : id; // clicar de novo volta ao vídeo
519
+ quadroNaTela = -1;
421
520
  render();
422
521
  });
423
522
  if (fotoSel == null) filme.scrollLeft = filme.scrollWidth;
@@ -436,7 +535,7 @@ function render() {
436
535
  + '<span class="txt">' + esc(e.acao) + (e.erro ? '<span class="erro">' + esc(e.erro) + '</span>' : '') + '</span>'
437
536
  + '<span class="hora">' + e.hora + '</span></div>';
438
537
  }).join('');
439
- document.querySelectorAll('.item.temfoto').forEach((el) => el.onclick = () => { fotoSel = Number(el.dataset.frame); render(); });
538
+ document.querySelectorAll('.item.temfoto').forEach((el) => el.onclick = () => { fotoSel = Number(el.dataset.frame); quadroNaTela = -1; render(); });
440
539
  }
441
540
 
442
541
  async function tick() {
@@ -450,9 +549,10 @@ async function tick() {
450
549
  } catch (e) {
451
550
  document.getElementById('st').textContent = 'reconectando…';
452
551
  }
453
- setTimeout(tick, 700);
552
+ setTimeout(tick, 600);
454
553
  }
455
554
  tick();
555
+ videoTick();
456
556
  </script>
457
557
  </body></html>`;
458
558
 
@@ -461,12 +561,13 @@ function start() {
461
561
  if (state.on) return Promise.resolve(state.url);
462
562
  try { fs.mkdirSync(DIR, { recursive: true }); } catch {}
463
563
  // Fotos de sessões passadas não pertencem a esta linha do tempo.
464
- try { for (const f of fs.readdirSync(DIR)) if (/^frame-\d+\.png$/.test(f)) fs.unlinkSync(path.join(DIR, f)); } catch {}
564
+ try { for (const f of fs.readdirSync(DIR)) if (/^(frame-\d+|live|live-tmp)\.png$/.test(f)) fs.unlinkSync(path.join(DIR, f)); } catch {}
465
565
 
466
566
  return new Promise((resolve, reject) => {
467
567
  const server = http.createServer((req, res) => {
468
568
  const u = new URL(req.url || '/', 'http://x');
469
569
  if (u.pathname === '/state') {
570
+ viuAgora(); // página aberta = vídeo ligado
470
571
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
471
572
  res.end(JSON.stringify({
472
573
  erroFoto: state.erroFoto,
@@ -474,9 +575,19 @@ function start() {
474
575
  frames: state.frames,
475
576
  log: state.log.slice(-120),
476
577
  instrucoes: state.instrucoes.slice(-20),
578
+ liveSeq: state.liveSeq,
477
579
  }));
478
580
  return;
479
581
  }
582
+ if (u.pathname === '/live') {
583
+ viuAgora();
584
+ fs.readFile(LIVE(), (err, buf) => {
585
+ if (err) { res.writeHead(404); res.end(); return; }
586
+ res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'no-store' });
587
+ res.end(buf);
588
+ });
589
+ return;
590
+ }
480
591
  if (u.pathname === '/frame') {
481
592
  // O id vem de fora: só serve o arquivo se for número — nada de
482
593
  // path traversal por aqui.
@@ -512,6 +623,7 @@ function start() {
512
623
 
513
624
  function stop() {
514
625
  if (state.server) { try { state.server.close(); } catch {} }
626
+ if (state.liveTimer) { clearTimeout(state.liveTimer); state.liveTimer = null; }
515
627
  state.server = null;
516
628
  state.on = false;
517
629
  state.url = '';
package/lib/ui.js CHANGED
@@ -161,9 +161,9 @@ const TIPS = [
161
161
  ];
162
162
 
163
163
  const NEWS = [
164
- 'Agora ele INTERAGE com o computador: janelas, botões e ícones pelo nome',
165
- '/streaming abre um painel ao vivo — veja cada passo dele na tela',
166
- 'Dois provedores em corrente: acabou a cota de um, o outro atende',
164
+ '/streaming agora é VÍDEO ao vivo — assista ele trabalhando, aba por aba',
165
+ 'Comandos se autopreenchem: /str + Tab (ou Enter) vira /streaming',
166
+ 'Ele INTERAGE com o computador: janelas, botões e ícones pelo nome',
167
167
  ];
168
168
 
169
169
  // ── Cabeçalho ────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "7.13.0",
3
+ "version": "7.14.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": {