primocode 9.8.0-beta.5 → 9.8.0-beta.7

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.
Files changed (52) hide show
  1. package/README.md +15 -4
  2. package/app/ansi.js +55 -0
  3. package/app/ide.css +345 -0
  4. package/app/ide.html +183 -0
  5. package/app/ide.js +639 -0
  6. package/bin/primocode.js +338 -88
  7. package/kits/acolhedor/index.html +130 -0
  8. package/kits/acolhedor/script.js +25 -0
  9. package/kits/acolhedor/style.css +146 -0
  10. package/kits/editorial/index.html +115 -0
  11. package/kits/editorial/script.js +19 -0
  12. package/kits/editorial/style.css +112 -0
  13. package/kits/premium/index.html +83 -0
  14. package/kits/premium/script.js +11 -0
  15. package/kits/premium/style.css +87 -0
  16. package/kits/vitrine/index.html +108 -0
  17. package/kits/vitrine/script.js +20 -0
  18. package/kits/vitrine/style.css +117 -0
  19. package/lib/act.js +66 -7
  20. package/lib/app.js +41 -6
  21. package/lib/browser.js +79 -3
  22. package/lib/checkpoint-git.js +218 -0
  23. package/lib/checkpoint.js +159 -44
  24. package/lib/comandos-extra.js +481 -0
  25. package/lib/config.js +5 -2
  26. package/lib/ide.js +337 -0
  27. package/lib/kits.js +165 -54
  28. package/lib/pre-leitura.js +118 -0
  29. package/lib/repeticao.js +4 -2
  30. package/lib/skills.js +6 -1
  31. package/lib/tempos.js +62 -0
  32. package/lib/ui.js +171 -17
  33. package/package.json +2 -2
  34. package/skills/apresentacao-pptx/SKILL.md +52 -0
  35. package/skills/cartaz-de-arte/LICENSE.txt +202 -0
  36. package/skills/cartaz-de-arte/SKILL.md +47 -0
  37. package/skills/criar-skill/LICENSE.txt +202 -0
  38. package/skills/criar-skill/SKILL.md +49 -0
  39. package/skills/depurar-de-verdade/LICENSE.txt +21 -0
  40. package/skills/depurar-de-verdade/SKILL.md +14 -5
  41. package/skills/documento-word/SKILL.md +57 -0
  42. package/skills/interface-revisada/SKILL.md +47 -0
  43. package/skills/pdf-bonito/SKILL.md +48 -0
  44. package/skills/planilha-excel/SKILL.md +47 -0
  45. package/skills/react-sem-dor/SKILL.md +48 -0
  46. package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
  47. package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
  48. package/skills/temas-prontos/LICENSE.txt +202 -0
  49. package/skills/temas-prontos/SKILL.md +50 -0
  50. package/skills/testar-o-site/LICENSE.txt +202 -0
  51. package/skills/testar-o-site/SKILL.md +46 -0
  52. package/skills/video-no-remotion/SKILL.md +48 -0
@@ -0,0 +1,11 @@
1
+ // Premium: um só movimento, na abertura. O arco sobe devagar quando a página
2
+ // carrega, e nada mais se mexe: animação em toda seção tira o ar de calma.
3
+ (function(){
4
+ var d=document;
5
+ d.documentElement.classList.add('js');
6
+ var arco=d.getElementById('arco');
7
+ if(!arco) return;
8
+ // Dois quadros de espera: garante que o navegador pintou o estado inicial
9
+ // antes de ligar a transição (senão ele pula direto para o fim).
10
+ requestAnimationFrame(function(){ requestAnimationFrame(function(){ arco.classList.add('pronto'); }); });
11
+ })();
@@ -0,0 +1,87 @@
1
+ :root{
2
+ /* ── A MARCA ───────────────────────────────────────────────────────
3
+ Para mudar a cara da página inteira, troque SÓ estas linhas:
4
+ --primaria a cor do arco e dos títulos (escura e sóbria)
5
+ --destaque o fio de luz no arco e os detalhes (um metal: latão,
6
+ cobre, prata)
7
+ --apoio as linhas finas e o fundo do depoimento
8
+ --fundo o fundo da página (claro e neutro)
9
+ --fonte-titulo / --fonte-texto as duas fontes (e o link do
10
+ Google Fonts no index.html)
11
+ O resto do arquivo usa estas variáveis: não precisa mexer nele. */
12
+ --primaria:#1f3a34;
13
+ --destaque:#b8955a;
14
+ --apoio:#dcd9d2;
15
+ --fundo:#f4f4f1;
16
+ --fonte-titulo:'Bodoni Moda',Georgia,serif;
17
+ --fonte-texto:'Manrope',system-ui,sans-serif;
18
+
19
+ --tinta:#1c1c1b;
20
+ --suave:#66655f;
21
+ }
22
+
23
+ /* Premium: muito espaço vazio e um único elemento marcante (o arco).
24
+ Tudo o mais é fino, calmo e centrado. O luxo aqui é o que NÃO tem. */
25
+ *{margin:0;padding:0;box-sizing:border-box;}
26
+ html{scroll-behavior:smooth;}
27
+ body{font-family:var(--fonte-texto);font-size:1.02rem;line-height:1.75;color:var(--tinta);background:var(--fundo);-webkit-font-smoothing:antialiased;}
28
+ a{color:inherit;}
29
+ :focus-visible{outline:2px solid var(--destaque);outline-offset:4px;}
30
+ h1,h2{font-family:var(--fonte-titulo);font-weight:400;line-height:1.12;color:var(--primaria);letter-spacing:-.01em;}
31
+ .pular{position:absolute;left:-9999px;top:8px;background:var(--tinta);color:var(--fundo);padding:8px 14px;z-index:10;}
32
+ .pular:focus{left:8px;}
33
+
34
+ .topo{display:flex;align-items:center;justify-content:space-between;padding:28px clamp(20px,5vw,64px);}
35
+ .marca{font-family:var(--fonte-titulo);font-size:1.6rem;font-weight:500;text-decoration:none;color:var(--primaria);}
36
+ .menu{display:flex;gap:32px;font-size:.95rem;}
37
+ .menu a{text-decoration:none;color:var(--suave);transition:color .3s;}
38
+ .menu a:hover{color:var(--tinta);}
39
+
40
+ .botao{display:inline-block;padding:14px 32px;border:1px solid var(--primaria);color:var(--primaria);font:500 .98rem var(--fonte-texto);text-decoration:none;letter-spacing:.01em;transition:background .35s,color .35s;}
41
+ .botao:hover{background:var(--primaria);color:var(--fundo);}
42
+ .botao-cheio{background:var(--primaria);color:var(--fundo);}
43
+ .botao-cheio:hover{background:transparent;color:var(--primaria);}
44
+
45
+ /* O arco: uma janela alta, com um fio de metal por dentro e um sol baixo */
46
+ .abertura{display:flex;flex-direction:column;align-items:center;text-align:center;padding:clamp(24px,5vw,56px) 24px clamp(90px,12vw,160px);}
47
+ .arco{position:relative;width:min(380px,70vw);aspect-ratio:3/4.2;border-radius:999px 999px 0 0;background:var(--primaria);overflow:hidden;margin-bottom:clamp(40px,6vw,72px);transform-origin:50% 100%;}
48
+ .arco::before{content:'';position:absolute;inset:18px 18px 0;border:1px solid var(--destaque);border-bottom:0;border-radius:999px 999px 0 0;opacity:.8;}
49
+ .arco-luz{position:absolute;left:50%;bottom:-18%;width:64%;aspect-ratio:1;transform:translateX(-50%);border-radius:50%;background:radial-gradient(circle at 50% 40%,var(--destaque),color-mix(in srgb,var(--destaque) 30%,var(--primaria)) 70%);opacity:.95;}
50
+ .abertura h1{font-size:clamp(2.2rem,4.8vw,4.1rem);max-width:18ch;margin-bottom:22px;}
51
+ .abertura-texto{color:var(--suave);font-size:1.1rem;margin-bottom:40px;}
52
+
53
+ /* Entrada: só o arco sobe, uma vez, ao abrir a página */
54
+ .js .arco{opacity:0;transform:scaleY(.86);}
55
+ .js .arco.pronto{opacity:1;transform:none;transition:opacity 1.4s ease,transform 1.6s cubic-bezier(.2,.7,.1,1);}
56
+
57
+ .manifesto{padding:clamp(80px,12vw,160px) 24px;border-top:1px solid var(--apoio);}
58
+ .manifesto p{max-width:30ch;margin:0 auto;text-align:center;font-family:var(--fonte-titulo);font-weight:400;font-size:clamp(1.5rem,2.7vw,2.2rem);line-height:1.4;color:var(--tinta);}
59
+
60
+ /* O trabalho: três colunas de texto com um fio em cima, sem caixa */
61
+ .trabalho{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(32px,5vw,80px);max-width:1160px;margin:0 auto;padding:clamp(60px,8vw,120px) clamp(24px,5vw,64px);}
62
+ .frente{border-top:1px solid var(--destaque);padding-top:28px;}
63
+ .frente h2{font-size:clamp(1.45rem,2vw,1.8rem);font-weight:400;margin-bottom:12px;}
64
+ .frente p{color:var(--suave);max-width:34ch;}
65
+
66
+ .depoimento{background:var(--apoio);padding:clamp(80px,11vw,150px) 24px;text-align:center;}
67
+ .depoimento p{font-family:var(--fonte-titulo);font-style:italic;font-weight:400;font-size:clamp(1.4rem,2.6vw,2.1rem);line-height:1.3;max-width:28ch;margin:0 auto;color:var(--primaria);}
68
+ .depoimento footer{margin-top:24px;color:var(--suave);font-size:.95rem;}
69
+
70
+ .contato{text-align:center;padding:clamp(90px,12vw,160px) 24px;}
71
+ .contato h2{font-size:clamp(2rem,4vw,3.2rem);margin-bottom:48px;}
72
+ .contato-grade{display:grid;grid-template-columns:repeat(3,auto);justify-content:center;gap:clamp(28px,6vw,96px);margin-bottom:56px;color:var(--suave);}
73
+ .contato-grade a{text-decoration:none;border-bottom:1px solid var(--apoio);transition:border-color .3s;}
74
+ .contato-grade a:hover{border-color:var(--destaque);}
75
+
76
+ .rodape{border-top:1px solid var(--apoio);padding:28px;text-align:center;font-size:.9rem;color:var(--suave);}
77
+
78
+ @media (max-width:760px){
79
+ .trabalho{grid-template-columns:1fr;}
80
+ .contato-grade{grid-template-columns:1fr;gap:20px;}
81
+ .menu{gap:20px;}
82
+ }
83
+ @media (prefers-reduced-motion:reduce){
84
+ html{scroll-behavior:auto;}
85
+ *{transition:none!important;}
86
+ .js .arco{opacity:1;transform:none;}
87
+ }
@@ -0,0 +1,108 @@
1
+ <!DOCTYPE html>
2
+ <html lang='pt-BR'>
3
+ <head>
4
+ <meta charset='UTF-8'>
5
+ <meta name='viewport' content='width=device-width, initial-scale=1.0'>
6
+ <!-- TROQUE: título e descrição com o nome e a oferta -->
7
+ <title>Sua Marca, a oferta que chama</title>
8
+ <meta name='description' content='Uma frase que diz o que é, quando e onde.'>
9
+ <link rel='preconnect' href='https://fonts.googleapis.com'>
10
+ <link rel='preconnect' href='https://fonts.gstatic.com' crossorigin>
11
+ <!-- TROQUE (se quiser): a fonte. Se trocar aqui, troque --fonte-titulo e --fonte-texto no topo do style.css -->
12
+ <link href='https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,800&display=swap' rel='stylesheet'>
13
+ <link rel='stylesheet' href='style.css'>
14
+ <script src='script.js' defer></script>
15
+ </head>
16
+ <body>
17
+ <a class='pular' href='#conteudo'>Pular para o conteúdo</a>
18
+
19
+ <!-- TROQUE: o nome e o link do pedido (WhatsApp, loja, ingresso) -->
20
+ <header class='topo'>
21
+ <a href='#' class='marca'>Sua Marca</a>
22
+ <a href='https://wa.me/5500000000000' class='botao botao-preto'>Quero o meu</a>
23
+ </header>
24
+
25
+ <main id='conteudo'>
26
+ <!-- TROQUE: o título curto e barulhento (poucas palavras), o selo e a frase de apoio -->
27
+ <section class='palco'>
28
+ <h1>Uma frase curta que dá fome.</h1>
29
+ <p class='selo'><span>Só nesta semana</span></p>
30
+ <div class='palco-base'>
31
+ <p class='palco-texto'>O que é, para quem é, e o motivo para pedir hoje. Duas frases, sem enrolar.</p>
32
+ <a href='https://wa.me/5500000000000' class='botao botao-preto'>Fazer meu pedido</a>
33
+ </div>
34
+ </section>
35
+
36
+ <!-- TROQUE: a data (no texto E no data-alvo, formato 2026-12-31T20:00), o lugar e o preço. Sem data? Apague o <p id='contagem'>. -->
37
+ <section class='quando' aria-label='Quando e onde'>
38
+ <div class='quando-data'>
39
+ <p class='quando-rotulo'>Quando</p>
40
+ <p class='quando-valor'>Sábado, 12 de dezembro</p>
41
+ <p id='contagem' class='contagem' data-alvo='2026-12-12T19:00'></p>
42
+ </div>
43
+ <div class='quando-lugar'>
44
+ <p class='quando-rotulo'>Onde</p>
45
+ <p class='quando-valor'>Rua Exemplo, 100, Sua Cidade</p>
46
+ </div>
47
+ <div class='quando-preco'>
48
+ <p class='quando-rotulo'>A partir de</p>
49
+ <p class='quando-valor'>R$ 00</p>
50
+ </div>
51
+ </section>
52
+
53
+ <!-- TROQUE: os produtos (ou atrações). O primeiro é a estrela e fica grande. Pode ter 4 a 6. -->
54
+ <section id='cardapio' class='mosaico' aria-label='O que tem'>
55
+ <article class='peca peca-estrela'>
56
+ <div class='arte arte-circulos' aria-hidden='true'></div>
57
+ <h2>O campeão da casa</h2>
58
+ <p>Por que todo mundo pede este. Uma frase que dá vontade.</p>
59
+ <p class='etiqueta'>R$ 00</p>
60
+ </article>
61
+ <article class='peca peca-azul'>
62
+ <div class='arte arte-meia' aria-hidden='true'></div>
63
+ <h3>Segundo item</h3>
64
+ <p>Uma frase curta e gostosa.</p>
65
+ <p class='etiqueta'>R$ 00</p>
66
+ </article>
67
+ <article class='peca peca-branca'>
68
+ <div class='arte arte-listras' aria-hidden='true'></div>
69
+ <h3>Terceiro item</h3>
70
+ <p>Uma frase curta e gostosa.</p>
71
+ <p class='etiqueta'>R$ 00</p>
72
+ </article>
73
+ <article class='peca peca-amarela'>
74
+ <div class='arte arte-gota' aria-hidden='true'></div>
75
+ <h3>Quarto item</h3>
76
+ <p>Uma frase curta e gostosa.</p>
77
+ <p class='etiqueta'>R$ 00</p>
78
+ </article>
79
+ <article class='peca peca-preta'>
80
+ <div class='arte arte-estrela' aria-hidden='true'></div>
81
+ <h3>Quinto item</h3>
82
+ <p>Uma frase curta e gostosa.</p>
83
+ <p class='etiqueta'>R$ 00</p>
84
+ </article>
85
+ </section>
86
+
87
+ <!-- TROQUE: como pedir, em três verbos (é uma sequência de verdade) e uma frase -->
88
+ <section class='passos'>
89
+ <p class='passos-verbos'>Escolha. <span>Chame.</span> Aproveite.</p>
90
+ <p class='passos-texto'>Uma frase que explica o caminho: onde pedir, em quanto tempo chega, como pagar.</p>
91
+ </section>
92
+
93
+ <!-- TROQUE: o último empurrão e os contatos -->
94
+ <section id='pedir' class='final'>
95
+ <h2>Bora?</h2>
96
+ <p>O convite final, com a urgência verdadeira (acaba quando?).</p>
97
+ <a href='https://wa.me/5500000000000' class='botao botao-preto'>Chamar no WhatsApp</a>
98
+ <p class='final-redes'><a href='https://instagram.com/'>@suamarca</a> no Instagram</p>
99
+ </section>
100
+ </main>
101
+
102
+ <!-- TROQUE: ano, nome e cidade -->
103
+ <footer class='rodape'>
104
+ <p>© 2026 Sua Marca, Sua Cidade</p>
105
+ <a href='#'>Voltar ao topo</a>
106
+ </footer>
107
+ </body>
108
+ </html>
@@ -0,0 +1,20 @@
1
+ // Vitrine: a pressa de verdade é a data. O script conta quantos dias faltam
2
+ // para o data-alvo e escreve em palavras ("Faltam 12 dias"), em vez de um
3
+ // relógio piscando. Se a data passou ou está vazia, o selo some sozinho.
4
+ (function(){
5
+ var el=document.getElementById('contagem');
6
+ if(!el) return;
7
+ var alvo=new Date(el.getAttribute('data-alvo')||'');
8
+ if(isNaN(alvo)) { el.textContent=''; return; }
9
+ function escrever(){
10
+ var ms=alvo-new Date();
11
+ if(ms<=0){ el.textContent=''; return; }
12
+ var dias=Math.floor(ms/864e5), horas=Math.floor(ms/36e5);
13
+ if(dias>1) el.textContent='Faltam '+dias+' dias';
14
+ else if(dias===1) el.textContent='É amanhã';
15
+ else if(horas>=1) el.textContent='Faltam '+horas+(horas===1?' hora':' horas');
16
+ else el.textContent='É agora';
17
+ }
18
+ escrever();
19
+ setInterval(escrever,6e4);
20
+ })();
@@ -0,0 +1,117 @@
1
+ :root{
2
+ /* ── A MARCA ───────────────────────────────────────────────────────
3
+ Para mudar a cara da página inteira, troque SÓ estas linhas:
4
+ --primaria o bloco de abertura e a peça estrela
5
+ --destaque o selo, as etiquetas de preço e o bloco final
6
+ --apoio a faixa de data e as peças de cor
7
+ --fonte-titulo / --fonte-texto a fonte (e o link do Google
8
+ Fonts no index.html)
9
+ Cores vivas e fortes funcionam melhor aqui. O resto do arquivo usa
10
+ estas variáveis: não precisa mexer nele. */
11
+ --primaria:#ffd23f;
12
+ --destaque:#ff4f8b;
13
+ --apoio:#2d5bff;
14
+ --fonte-titulo:'Bricolage Grotesque',system-ui,sans-serif;
15
+ --fonte-texto:'Bricolage Grotesque',system-ui,sans-serif;
16
+
17
+ --tinta:#141414;
18
+ --branco:#ffffff;
19
+ --lado:clamp(16px,3vw,40px);
20
+ }
21
+
22
+ /* Vitrine: blocos de cor chapada, tipo enorme e contorno grosso, como
23
+ cartaz de rua. Nada de sombra suave nem vidro: tudo é plano e forte. */
24
+ *{margin:0;padding:0;box-sizing:border-box;}
25
+ html{scroll-behavior:smooth;}
26
+ body{font-family:var(--fonte-texto);font-size:1.1rem;line-height:1.5;color:var(--tinta);background:var(--branco);-webkit-font-smoothing:antialiased;overflow-x:hidden;}
27
+ a{color:inherit;}
28
+ :focus-visible{outline:4px solid var(--apoio);outline-offset:3px;}
29
+ h1,h2,h3{font-family:var(--fonte-titulo);font-weight:800;line-height:.95;letter-spacing:-.035em;}
30
+ .pular{position:absolute;left:-9999px;top:8px;background:var(--tinta);color:var(--branco);padding:8px 14px;z-index:10;}
31
+ .pular:focus{left:8px;}
32
+
33
+ .topo{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px var(--lado);background:var(--primaria);border-bottom:3px solid var(--tinta);}
34
+ .marca{font-family:var(--fonte-titulo);font-weight:800;font-size:1.6rem;letter-spacing:-.04em;text-decoration:none;}
35
+
36
+ .botao{display:inline-block;padding:14px 26px;border:3px solid var(--tinta);border-radius:14px;background:var(--branco);color:var(--tinta);font:800 1.05rem var(--fonte-titulo);text-decoration:none;box-shadow:5px 5px 0 var(--tinta);transition:transform .15s,box-shadow .15s;}
37
+ .botao:hover{transform:translate(-2px,-2px);box-shadow:7px 7px 0 var(--tinta);}
38
+ .botao:active{transform:translate(3px,3px);box-shadow:2px 2px 0 var(--tinta);}
39
+ .botao-preto{background:var(--tinta);color:var(--branco);box-shadow:5px 5px 0 var(--destaque);}
40
+ .botao-preto:hover{box-shadow:7px 7px 0 var(--destaque);}
41
+ .botao-preto:active{box-shadow:2px 2px 0 var(--destaque);}
42
+
43
+ /* Palco: o título ocupa a largura toda, e o selo gira no canto */
44
+ .palco{position:relative;background:var(--primaria);padding:clamp(40px,6vw,80px) var(--lado) clamp(48px,6vw,80px);border-bottom:3px solid var(--tinta);overflow:hidden;}
45
+ .palco h1{font-size:clamp(3.4rem,12.5vw,13rem);max-width:10ch;position:relative;z-index:1;}
46
+ .selo{position:absolute;right:clamp(16px,6vw,110px);top:clamp(24px,5vw,70px);width:clamp(120px,15vw,210px);aspect-ratio:1;display:grid;place-items:center;text-align:center;border-radius:50%;background:var(--destaque);font-family:var(--fonte-titulo);font-weight:800;font-size:clamp(1rem,1.6vw,1.45rem);line-height:1.05;transform:rotate(-12deg);z-index:2;
47
+ /* a borda serrilhada do selo: um polígono de 24 pontas */
48
+ clip-path:polygon(50% 0%,56.5% 7.4%,65.4% 3.4%,68.8% 12.6%,78.6% 12.4%,78.3% 22.1%,87.6% 25%,84.1% 34.1%,92.1% 39.8%,86.4% 47.6%,92.1% 55.8%,84.1% 61%,87.6% 70%,78.3% 73%,78.6% 82.7%,68.8% 82.4%,65.4% 91.6%,56.5% 87.6%,50% 95%,43.5% 87.6%,34.6% 91.6%,31.2% 82.4%,21.4% 82.7%,21.7% 73%,12.4% 70%,15.9% 61%,7.9% 55.8%,13.6% 47.6%,7.9% 39.8%,15.9% 34.1%,12.4% 25%,21.7% 22.1%,21.4% 12.4%,31.2% 12.6%,34.6% 3.4%,43.5% 7.4%);}
49
+ .selo span{max-width:8ch;}
50
+ .palco-base{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:24px;margin-top:clamp(28px,4vw,48px);}
51
+ .palco-texto{font-size:clamp(1.2rem,1.8vw,1.5rem);font-weight:600;max-width:34ch;}
52
+
53
+ /* Quando e onde: uma faixa de três colunas com filete preto entre elas */
54
+ .quando{display:grid;grid-template-columns:1.3fr 1.3fr 1fr;background:var(--apoio);color:var(--branco);border-bottom:3px solid var(--tinta);}
55
+ .quando>div{padding:28px var(--lado);}
56
+ .quando>div+div{border-left:3px solid var(--tinta);}
57
+ .quando-rotulo{font-weight:600;opacity:.85;}
58
+ .quando-valor{font-family:var(--fonte-titulo);font-weight:800;font-size:clamp(1.5rem,2.6vw,2.3rem);line-height:1.05;letter-spacing:-.02em;margin-top:4px;}
59
+ .contagem{margin-top:10px;display:inline-block;background:var(--primaria);color:var(--tinta);font-weight:800;padding:4px 12px;border-radius:999px;border:2px solid var(--tinta);}
60
+ .contagem:empty{display:none;}
61
+
62
+ /* Mosaico: peças de tamanhos e cores diferentes, e não cards iguais */
63
+ .mosaico{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:minmax(250px,auto);gap:3px;background:var(--tinta);border-bottom:3px solid var(--tinta);}
64
+ .peca{position:relative;display:flex;flex-direction:column;justify-content:flex-end;gap:8px;padding:28px;overflow:hidden;background:var(--branco);}
65
+ .peca h2,.peca h3,.peca p{position:relative;z-index:1;}
66
+ .peca h3{font-size:clamp(1.6rem,2.4vw,2.2rem);}
67
+ .peca p{max-width:28ch;font-weight:600;}
68
+ .peca-estrela{grid-column:span 2;grid-row:span 2;background:var(--destaque);}
69
+ .peca-estrela h2{font-size:clamp(2.6rem,5.5vw,5.2rem);max-width:9ch;}
70
+ .peca-estrela p{font-size:1.25rem;}
71
+ .peca-azul{background:var(--apoio);color:var(--branco);}
72
+ .peca-amarela{background:var(--primaria);}
73
+ .peca-preta{background:var(--tinta);color:var(--branco);}
74
+ .etiqueta{position:absolute!important;top:22px;right:22px;background:var(--branco);color:var(--tinta);font-family:var(--fonte-titulo);font-weight:800;font-size:1.3rem;padding:6px 14px;border:3px solid var(--tinta);border-radius:10px;transform:rotate(4deg);}
75
+ .peca-estrela .etiqueta{font-size:2rem;padding:8px 18px;transform:rotate(-5deg);}
76
+
77
+ /* As artes: formas geométricas no lugar da foto (o pedido pode não ter foto) */
78
+ .arte{position:absolute;z-index:0;}
79
+ .arte-circulos{right:-8%;top:10%;width:62%;aspect-ratio:1;border-radius:50%;background:radial-gradient(circle,var(--primaria) 0 22%,transparent 22.5% 36%,var(--primaria) 36.5% 50%,transparent 50.5%);}
80
+ .arte-meia{right:-30px;top:-30px;width:170px;aspect-ratio:1;border-radius:50%;background:var(--primaria);}
81
+ .arte-listras{left:0;right:0;top:0;height:46%;background:repeating-linear-gradient(-45deg,var(--destaque) 0 16px,transparent 16px 32px);opacity:.9;}
82
+ .arte-gota{right:24px;top:70px;width:110px;aspect-ratio:1;border-radius:50% 0 50% 50%;background:var(--destaque);transform:rotate(-45deg);}
83
+ .arte-estrela{right:22px;top:64px;width:120px;aspect-ratio:1;background:var(--primaria);clip-path:polygon(50% 0,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%);}
84
+
85
+ /* Passos: três verbos em uma linha enorme */
86
+ .passos{padding:clamp(56px,8vw,110px) var(--lado);border-bottom:3px solid var(--tinta);}
87
+ .passos-verbos{font-family:var(--fonte-titulo);font-weight:800;font-size:clamp(2.6rem,9vw,8.5rem);line-height:.95;letter-spacing:-.04em;}
88
+ .passos-verbos span{color:var(--apoio);}
89
+ .passos-texto{margin-top:24px;font-size:1.3rem;font-weight:600;max-width:40ch;}
90
+
91
+ .final{background:var(--destaque);padding:clamp(56px,8vw,110px) var(--lado);text-align:center;border-bottom:3px solid var(--tinta);}
92
+ .final h2{font-size:clamp(4.5rem,16vw,14rem);}
93
+ .final p{font-size:1.3rem;font-weight:600;margin:16px auto 28px;max-width:34ch;}
94
+ .final .botao-preto{box-shadow:5px 5px 0 var(--primaria);}
95
+ .final-redes{font-size:1.05rem!important;margin-top:28px!important;}
96
+
97
+ .rodape{display:flex;justify-content:space-between;gap:16px;padding:22px var(--lado);background:var(--tinta);color:var(--branco);font-weight:600;}
98
+
99
+ @media (max-width:960px){
100
+ .mosaico{grid-template-columns:1fr 1fr;}
101
+ .peca-estrela{grid-column:span 2;grid-row:span 1;min-height:380px;}
102
+ }
103
+ @media (max-width:640px){
104
+ .quando{grid-template-columns:1fr;}
105
+ .quando>div+div{border-left:0;border-top:3px solid var(--tinta);}
106
+ .mosaico{grid-template-columns:1fr;}
107
+ .peca-estrela,.peca-preta{grid-column:auto;}
108
+ .peca{min-height:230px;}
109
+ .peca-estrela{min-height:400px;padding-top:110px;}
110
+ .selo{position:relative;right:auto;top:auto;width:116px;margin-top:22px;}
111
+ .arte-circulos{width:46%;top:22%;right:-12%;}
112
+ .palco{padding-bottom:40px;}
113
+ .palco-base{flex-direction:column;align-items:flex-start;}
114
+ .rodape{flex-direction:column;}
115
+ .topo .botao{padding:10px 16px;font-size:.95rem;}
116
+ }
117
+ @media (prefers-reduced-motion:reduce){html{scroll-behavior:auto;}*{transition:none!important;}}
package/lib/act.js CHANGED
@@ -24,6 +24,8 @@ const permissao = require('./permissao');
24
24
  const perigo = require('./perigo');
25
25
  const skills = require('./skills');
26
26
  const checkpoint = require('./checkpoint');
27
+ const tempos = require('./tempos');
28
+ const preLeitura = require('./pre-leitura');
27
29
 
28
30
  // 40, não 15: com o freio de repetição (repeticao.js) e o de rodada parada,
29
31
  // volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
@@ -35,6 +37,8 @@ const MAX_EMPURROES = 3;
35
37
  // Voltas seguidas sem executar nada antes de desistir.
36
38
  const MAX_RODADAS_PARADAS = 3;
37
39
  const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
40
+ // As leituras que, repetidas sem mudança no meio, devolvem o guardado.
41
+ const RELEITURA = new Set(['read_file', 'list_dir']);
38
42
 
39
43
  // ── Interrupção (Esc Esc) ─────────────────────────────────────────────────
40
44
  // O usuário aperta Esc duas vezes e o agente PARA — entre uma rodada e outra
@@ -382,6 +386,7 @@ async function pausar(ms, subagent) {
382
386
  const sp = subagent ? null : new Spinner(PENSANDO).start();
383
387
  await new Promise((r) => setTimeout(r, ms));
384
388
  if (sp) sp.stop();
389
+ tempos.somar('espera de cota/rede', ms);
385
390
  }
386
391
 
387
392
  /**
@@ -451,10 +456,10 @@ function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
451
456
  const PULAR_NO_MAPA = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', 'out',
452
457
  '.cache', 'coverage', '__pycache__', '.venv', 'venv', '.turbo', '.vercel', '.idea', '.vscode']);
453
458
 
454
- function mapaDoProjeto(dir, limite = 80) {
459
+ function arquivosDoProjeto(dir, limite = 80) {
455
460
  const path = require('path');
456
461
  const fs = require('fs');
457
- if (!dir || path.resolve(dir) === path.resolve(cfg.ROOT_DIR)) return '';
462
+ if (!dir || path.resolve(dir) === path.resolve(cfg.ROOT_DIR)) return [];
458
463
  const achados = [];
459
464
  const andar = (atual, fundo) => {
460
465
  if (fundo > 4 || achados.length >= limite) return;
@@ -470,12 +475,18 @@ function mapaDoProjeto(dir, limite = 80) {
470
475
  }
471
476
  };
472
477
  andar(dir, 0);
478
+ return achados;
479
+ }
480
+
481
+ function mapaDoProjeto(dir, limite = 80, achados = arquivosDoProjeto(dir, limite)) {
473
482
  if (!achados.length) return '';
474
483
  const mais = achados.length >= limite ? `\n… (mais arquivos — use glob para achar)` : '';
475
484
  return `Arquivos que JÁ existem neste projeto (edite-os em vez de recriar):\n${achados.join('\n')}${mais}`;
476
485
  }
477
486
 
478
- function buildMemoryContext(projectDir, projectName) {
487
+ /* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
488
+ entrar já lido (ver lib/pre-leitura.js). */
489
+ function buildMemoryContext(projectDir, projectName, pedido = '') {
479
490
  const parts = [];
480
491
  // As instruções vêm primeiro: são regra de quem manda no projeto, e a
481
492
  // memória é o que o agente anotou. Entram aqui (e não no bin) para valer
@@ -486,8 +497,11 @@ function buildMemoryContext(projectDir, projectName) {
486
497
  if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
487
498
  const projeto = cfg.readProjectMemory(projectDir);
488
499
  if (projeto.trim()) parts.push(`[${projectName || 'projeto'}/memory.md]\n${projeto}`);
489
- const mapa = mapaDoProjeto(projectDir);
490
- if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}`);
500
+ const arquivos = arquivosDoProjeto(projectDir);
501
+ const mapa = mapaDoProjeto(projectDir, 80, arquivos);
502
+ if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}\n\n${preLeitura.JUNTAR_LEITURAS}`);
503
+ const lidos = preLeitura.bloco(preLeitura.escolher(projectDir, arquivos, pedido));
504
+ if (lidos) parts.push(lidos);
491
505
  return parts.join('\n\n');
492
506
  }
493
507
 
@@ -520,6 +534,11 @@ async function runAct(userPrompt, opts) {
520
534
  /* Quem abre a página para conferir antes do "pronto". Injetável pelo
521
535
  mesmo motivo do `motor`: o teste mede a regra sem abrir um Chrome. */
522
536
  medirPagina = (dir) => require('./pipeline.js').medirNoNavegador(dir),
537
+ /* Quem LIGA o navegador escondido antes da hora. Injetável pelo mesmo
538
+ motivo do `medirPagina` — e só vale junto com o padrão dele: um
539
+ teste que troca o medidor não pode subir um Chrome de verdade. */
540
+ aquecerPagina = opts && opts.medirPagina ? null
541
+ : () => require('./browser.js').aquecerRevisor(),
523
542
  } = opts || {};
524
543
 
525
544
  // A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
@@ -550,6 +569,7 @@ async function runAct(userPrompt, opts) {
550
569
  let mexeuNaPagina = false; // mexeu desde a última conferência?
551
570
  let conferencias = 0; // até duas: entregar, e conferir o conserto
552
571
  let avisoDaPagina = ''; // o que ainda estava quebrado na última
572
+ let msConferindo = 0; // para o PRIMOCODE_TEMPOS separar as fases
553
573
  /* Abre a página e devolve o relato quando é para VOLTAR (achou problema e
554
574
  ainda cabe uma volta); null quando é para entregar. O que ficar
555
575
  quebrado na última conferência fica em `avisoDaPagina`, e a entrega sai
@@ -560,10 +580,13 @@ async function runAct(userPrompt, opts) {
560
580
  conferencias++;
561
581
  mexeuNaPagina = false;
562
582
  let medida = null;
583
+ const t = Date.now();
563
584
  try {
564
585
  fala.pensamento('abrindo a página para conferir antes de entregar', profundidade);
565
586
  medida = await medirPagina(pastaAtual);
566
587
  } catch { medida = null; }
588
+ msConferindo += Date.now() - t;
589
+ tempos.somar('conferir página', Date.now() - t);
567
590
  if (!medida || !medida.testou) return null;
568
591
  avisoDaPagina = medida.limpo ? ''
569
592
  : String(medida.relato || '').split('\n').filter((l) => /·|BRANCO/.test(l)).slice(0, 3).join(' ');
@@ -574,6 +597,8 @@ async function runAct(userPrompt, opts) {
574
597
  ? `${texto}\n\nAtenção: abri a página e ela ainda tem problema — ${avisoDaPagina.trim()}` : texto);
575
598
  // Quem já foi chamado, e quantas vezes. Ver lib/repeticao.js.
576
599
  const jaChamado = repeticao.novo();
600
+ // O que read_file/list_dir devolveram, pela chave da época (repeticao.js).
601
+ const leiturasGuardadas = new Map();
577
602
  /* O livro deste pedido: arquivos, comandos, links, testes e falhas. Sai
578
603
  no fim como balanço (ver lib/registro.js). O subagente tem o seu, que
579
604
  não se mistura com o do principal — mas o que ele fez também precisa
@@ -623,7 +648,7 @@ async function runAct(userPrompt, opts) {
623
648
 
624
649
  const messages = [];
625
650
  if (!subagent) {
626
- const memory = buildMemoryContext(projectDir, projectName);
651
+ const memory = buildMemoryContext(projectDir, projectName, userPrompt);
627
652
  if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
628
653
  for (const m of (history || []).slice(-20)) {
629
654
  if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
@@ -735,6 +760,7 @@ async function runAct(userPrompt, opts) {
735
760
  let fluxo = null;
736
761
 
737
762
  emVoo = new AbortController();
763
+ const tRodada = Date.now();
738
764
  try {
739
765
  await motor.requestAct(server, { messages: compactar(messages), model, effort, token, projectDir: pastaAtual, projectName, subagent, sinal: emVoo.signal }, (evt) => {
740
766
  if (evt.erro) { streamError = evt.erro; return; }
@@ -781,6 +807,7 @@ async function runAct(userPrompt, opts) {
781
807
  }
782
808
  sp.stop();
783
809
  emVoo = null;
810
+ tempos.somar(subagent ? 'rodada do subagente' : 'rodada do modelo', Date.now() - tRodada);
784
811
  if (PARAR) { if (fluxo) fluxo.fechar(); break; }
785
812
 
786
813
  // Chamada de função malformada: o modelo errou a SINTAXE da chamada,
@@ -957,6 +984,8 @@ async function runAct(userPrompt, opts) {
957
984
 
958
985
  let finished = null;
959
986
  let algoRodou = false;
987
+ const tTools = Date.now();
988
+ const conferindoAntes = msConferindo;
960
989
 
961
990
  // ORQUESTRAÇÃO: subagentes da MESMA rodada rodam juntos. O modelo que
962
991
  // pede "3 agentes, um por arquivo" esperava paralelismo e recebia uma
@@ -1064,7 +1093,18 @@ async function runAct(userPrompt, opts) {
1064
1093
  const eco = repeticao.ver(jaChamado, tc.name, tc.arguments);
1065
1094
  if (eco.repetida) {
1066
1095
  voltasEmFalso++;
1067
- messages.push({ role: 'tool', tool_call_id: tc.id, content: eco.aviso });
1096
+ /* RELER SEM MUDANÇA devolve o que já foi lido, uma vez.
1097
+ O aviso seco ("o resultado está na conversa acima") mentia
1098
+ depois do `compactar`: a leitura velha já tinha virado 300
1099
+ caracteres, e o modelo gastava OUTRA ida ao servidor para
1100
+ achar o arquivo por outro caminho (um `cat`, um grep). Na
1101
+ segunda vez vai o conteúdo guardado — ler de novo sem nada
1102
+ mudar dá o mesmo texto, então não há o que executar. Da
1103
+ terceira em diante é laço, e o freio segue como era. */
1104
+ const guardado = eco.vezes === 2 && leiturasGuardadas.get(eco.chave);
1105
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: guardado
1106
+ ? `(sem mudança desde a última leitura — é o mesmo conteúdo; não precisa ler de novo)\n${guardado}`
1107
+ : eco.aviso });
1068
1108
  continue;
1069
1109
  }
1070
1110
 
@@ -1125,6 +1165,9 @@ async function runAct(userPrompt, opts) {
1125
1165
  const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
1126
1166
  logResult(tc.name, result, profundidade);
1127
1167
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1168
+ if (RELEITURA.has(tc.name) && result && result.ok !== false) {
1169
+ leiturasGuardadas.set(eco.chave, summarizeForModel(result));
1170
+ }
1128
1171
  if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
1129
1172
  // Além de narrar, ANOTA: no fim do pedido sai o balanço do que
1130
1173
  // nasceu, mudou, rodou e falhou. Narrar sozinho não basta — numa
@@ -1133,6 +1176,19 @@ async function runAct(userPrompt, opts) {
1133
1176
  if (result && result.ok !== false && catalogo.criaProjeto(tc.name, tc.arguments)
1134
1177
  && /\.(html?|css|m?jsx?|tsx?|vue|svelte)$/i.test(String((tc.arguments && tc.arguments.path) || ''))) {
1135
1178
  mexeuNaPagina = true;
1179
+ /* O NAVEGADOR ESCONDIDO SOBE AGORA, e não no `finish`.
1180
+ A conferência só roda quando o agente diz "pronto", e até
1181
+ lá ainda há pelo menos uma rodada do modelo. Subir o Chrome
1182
+ ali, em paralelo com ela, tira do "pronto" o tempo de ligar
1183
+ o navegador e de fazer a PRIMEIRA navegação dele, que é a
1184
+ parte cara (ver `aquecerRevisor` em lib/browser.js). Medido
1185
+ em 24/09: a conferência num Chrome recém-aberto levava de
1186
+ 2,1 a 4,6s; aquecido antes, 1,24s — que é o assentar de
1187
+ 1,2s, e nada mais. Sem `await` e sem erro: se não subir, a
1188
+ conferência tenta de novo e diz o que houve. */
1189
+ if (aquecerPagina && !subagent && conferencias < 2) {
1190
+ try { Promise.resolve(aquecerPagina()).catch(() => {}); } catch { /* segue */ }
1191
+ }
1136
1192
  }
1137
1193
 
1138
1194
  // Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
@@ -1174,6 +1230,9 @@ async function runAct(userPrompt, opts) {
1174
1230
  messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
1175
1231
  }
1176
1232
 
1233
+ // A conferência da página tem linha própria: aqui fica só o resto.
1234
+ tempos.somar('ferramentas', Date.now() - tTools - (msConferindo - conferindoAntes));
1235
+
1177
1236
  // Uma volta inteira sem executar nada é o modelo preso. Insistir custa
1178
1237
  // um pedido ao servidor por volta, e as 15 voltas saíam caras para
1179
1238
  // terminar no mesmo lugar — melhor parar e devolver a vez ao usuário.