primocode 9.8.0-beta.4 → 9.8.0-beta.6

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 +315 -83
  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/app.js +41 -6
  20. package/lib/browser.js +14 -1
  21. package/lib/checkpoint-git.js +218 -0
  22. package/lib/checkpoint.js +159 -44
  23. package/lib/comandos-extra.js +481 -0
  24. package/lib/config.js +5 -2
  25. package/lib/ide.js +337 -0
  26. package/lib/kits.js +165 -54
  27. package/lib/skills.js +6 -1
  28. package/lib/ui.js +171 -17
  29. package/package.json +2 -2
  30. package/skills/apresentacao-pptx/SKILL.md +52 -0
  31. package/skills/cartaz-de-arte/LICENSE.txt +202 -0
  32. package/skills/cartaz-de-arte/SKILL.md +47 -0
  33. package/skills/criar-skill/LICENSE.txt +202 -0
  34. package/skills/criar-skill/SKILL.md +49 -0
  35. package/skills/depurar-de-verdade/LICENSE.txt +21 -0
  36. package/skills/depurar-de-verdade/SKILL.md +14 -5
  37. package/skills/documento-word/SKILL.md +57 -0
  38. package/skills/interface-revisada/SKILL.md +47 -0
  39. package/skills/me-ensina/LICENSE.txt +21 -0
  40. package/skills/me-ensina/SKILL.md +27 -0
  41. package/skills/me-entrevista/LICENSE.txt +21 -0
  42. package/skills/me-entrevista/SKILL.md +27 -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
package/app/ide.js ADDED
@@ -0,0 +1,639 @@
1
+ /* A PrimoCode IDE conversando com o PrimoCode.
2
+ *
3
+ * "Um app ao invés de CLI, aí fica uma IDE e uma CLI."
4
+ *
5
+ * Sem framework, como a janela do /rc. Quatro partes, cada uma com a sua
6
+ * seção abaixo: a árvore (lista da pasta do projeto), o editor (um textarea
7
+ * com calha de linhas), a prévia (iframe que recarrega sozinho) e o chat +
8
+ * terminal (o mesmo fluxo SSE do /rc).
9
+ *
10
+ * O que amarra as quatro é o evento `arquivos` do servidor: o agente escreve
11
+ * um arquivo, o vigia de lib/ide.js avisa, e a árvore acende, a aba aberta
12
+ * se atualiza e a prévia recarrega — sem a pessoa clicar em nada.
13
+ */
14
+ (function () {
15
+ "use strict";
16
+
17
+ // ── chave ────────────────────────────────────────────────────────────
18
+ // Mesmo trato do app.js: sai da URL assim que é lida, mas fica no
19
+ // sessionStorage para o F5 não matar a janela.
20
+ var CHAVE = new URLSearchParams(location.search).get("k") || "";
21
+ try {
22
+ if (CHAVE) sessionStorage.setItem("primo-chave", CHAVE);
23
+ else CHAVE = sessionStorage.getItem("primo-chave") || "";
24
+ } catch (e) { /* vale só esta abertura */ }
25
+ if (location.search) history.replaceState(null, "", location.pathname);
26
+
27
+ var $ = function (id) { return document.getElementById(id); };
28
+
29
+ function pedir(rota, corpo) {
30
+ return fetch(rota, {
31
+ method: corpo === undefined ? "GET" : "POST",
32
+ headers: { "Content-Type": "application/json", "X-Primo-Chave": CHAVE },
33
+ body: corpo === undefined ? undefined : JSON.stringify(corpo),
34
+ }).then(function (r) {
35
+ return r.json().catch(function () { return {}; }).then(function (d) { d.__status = r.status; return d; });
36
+ });
37
+ }
38
+
39
+ // ── tema (o mesmo que a janela do /rc guardou) ───────────────────────
40
+ (function aplicarTema() {
41
+ var tema = "escuro", cor = "ciano";
42
+ try { tema = localStorage.getItem("primocode_tema") || tema; cor = localStorage.getItem("primocode_cor") || cor; } catch (e) {}
43
+ if (tema === "sistema") tema = matchMedia("(prefers-color-scheme: dark)").matches ? "escuro" : "claro";
44
+ document.documentElement.setAttribute("data-tema", tema);
45
+ document.documentElement.setAttribute("data-cor", cor);
46
+ })();
47
+ if (window.PrimoMarca) {
48
+ var desenha = window.PrimoMarca.logo || window.PrimoMarca.svg;
49
+ $("marcaTopo").innerHTML = desenha({ tamanho: 20 });
50
+ $("marcaGrande").innerHTML = desenha({ tamanho: 64, rotulo: "PrimoCode" });
51
+ }
52
+
53
+ // ── estado da página ─────────────────────────────────────────────────
54
+ var projeto = { pasta: "", nome: "", itens: [], entrada: null };
55
+ var abertas = []; // abas: { caminho, tipo, conteudo, original, mtime, rolagem, selecao }
56
+ var ativa = null; // caminho da aba ativa
57
+ var expandidas = {}; // pastas abertas na árvore
58
+ var mexidos = {}; // caminho → quando o agente mexeu (para acender)
59
+ var previaLigada = false;
60
+
61
+ function extensao(c) { var m = /\.([a-z0-9]+)$/i.exec(c || ""); return m ? m[1].toLowerCase() : ""; }
62
+ function nomeDe(c) { return String(c).split("/").pop(); }
63
+ var IMAGENS = { png: 1, jpg: 1, jpeg: 1, gif: 1, webp: 1, svg: 1, ico: 1 };
64
+ var LINGUAS = { html: "HTML", htm: "HTML", css: "CSS", js: "JavaScript", mjs: "JavaScript", ts: "TypeScript",
65
+ json: "JSON", md: "Markdown", py: "Python", txt: "Texto", svg: "SVG", sh: "Shell", yml: "YAML", yaml: "YAML" };
66
+
67
+ /* ── árvore ─────────────────────────────────────────────────────────── */
68
+
69
+ function carregarArvore() {
70
+ return pedir("/api/ide/arvore").then(function (d) {
71
+ if (!d.ok) { $("arvorePe").textContent = d.error || "não consegui ler a pasta"; return; }
72
+ var trocou = projeto.pasta && projeto.pasta !== d.pasta;
73
+ projeto = d;
74
+ $("nomeProjeto").textContent = d.nome || "projeto";
75
+ $("pastaProjeto").textContent = d.pasta;
76
+ $("arvorePe").textContent = d.cortada ? "Lista cortada em 4.000 itens — a pasta é grande demais para mostrar inteira." : "";
77
+ if (trocou) fecharTudo();
78
+ desenharArvore();
79
+ pintarPreviaVazia();
80
+ }).catch(function () {});
81
+ }
82
+
83
+ function filhosDe(pai) {
84
+ var prefixo = pai ? pai + "/" : "";
85
+ return projeto.itens.filter(function (i) {
86
+ if (i.caminho.indexOf(prefixo) !== 0) return false;
87
+ return i.caminho.slice(prefixo.length).indexOf("/") === -1;
88
+ });
89
+ }
90
+
91
+ function desenharArvore() {
92
+ var lista = $("lista");
93
+ var rolagem = lista.scrollTop;
94
+ lista.innerHTML = "";
95
+ if (!projeto.itens.length) {
96
+ var v = document.createElement("div");
97
+ v.className = "arvore-pe";
98
+ v.style.border = "0";
99
+ v.textContent = "Pasta vazia. Peça ao Primo para criar alguma coisa — os arquivos aparecem aqui na hora.";
100
+ lista.appendChild(v);
101
+ return;
102
+ }
103
+ (function andar(pai, nivel) {
104
+ filhosDe(pai).forEach(function (item) {
105
+ lista.appendChild(linhaDaArvore(item, nivel));
106
+ if (item.tipo === "pasta" && expandidas[item.caminho]) andar(item.caminho, nivel + 1);
107
+ });
108
+ })("", 0);
109
+ lista.scrollTop = rolagem;
110
+ }
111
+
112
+ function linhaDaArvore(item, nivel) {
113
+ var b = document.createElement("button");
114
+ b.className = "no" + (item.tipo === "pasta" ? " pasta" : "");
115
+ b.style.setProperty("--nivel", nivel);
116
+ b.setAttribute("role", "treeitem");
117
+ b.dataset.caminho = item.caminho;
118
+ b.title = item.caminho;
119
+ var seta = document.createElement("span");
120
+ seta.className = "seta";
121
+ var tipo = document.createElement("span");
122
+ tipo.className = "tipo";
123
+ if (item.tipo === "pasta") {
124
+ b.setAttribute("aria-expanded", expandidas[item.caminho] ? "true" : "false");
125
+ seta.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
126
+ b.appendChild(seta);
127
+ b.onclick = function () { expandidas[item.caminho] = !expandidas[item.caminho]; desenharArvore(); };
128
+ } else {
129
+ var ext = extensao(item.caminho);
130
+ tipo.textContent = ext ? ext.slice(0, 4) : "·";
131
+ tipo.className = "tipo t-" + (IMAGENS[ext] ? "img" : ext);
132
+ b.appendChild(seta);
133
+ b.appendChild(tipo);
134
+ b.setAttribute("aria-selected", item.caminho === ativa ? "true" : "false");
135
+ b.onclick = function () { irPara(item.caminho); };
136
+ }
137
+ if (mexidos[item.caminho] && Date.now() - mexidos[item.caminho] < 8000) b.classList.add("mexido");
138
+ var nome = document.createElement("span");
139
+ nome.className = "nome-no";
140
+ nome.textContent = nomeDe(item.caminho);
141
+ b.appendChild(nome);
142
+ return b;
143
+ }
144
+
145
+ // Mostra o arquivo que o agente acabou de criar: abre as pastas até ele.
146
+ function revelar(caminho) {
147
+ var partes = caminho.split("/");
148
+ for (var i = 1; i < partes.length; i++) expandidas[partes.slice(0, i).join("/")] = true;
149
+ }
150
+
151
+ /* ── abas ───────────────────────────────────────────────────────────── */
152
+
153
+ function aba(caminho) {
154
+ for (var i = 0; i < abertas.length; i++) if (abertas[i].caminho === caminho) return abertas[i];
155
+ return null;
156
+ }
157
+ function suja(a) { return a && a.tipo === "texto" && a.conteudo !== a.original; }
158
+
159
+ function desenharAbas() {
160
+ var rolo = $("abasRolo");
161
+ rolo.innerHTML = "";
162
+ abertas.forEach(function (a) {
163
+ var b = document.createElement("div");
164
+ b.className = "aba" + (suja(a) ? " sujo" : "");
165
+ b.setAttribute("role", "tab");
166
+ b.setAttribute("tabindex", "0");
167
+ b.setAttribute("aria-selected", a.caminho === ativa ? "true" : "false");
168
+ b.title = a.caminho + (suja(a) ? " — não salvo" : "");
169
+ var ext = extensao(a.caminho);
170
+ var t = document.createElement("span");
171
+ t.className = "tipo t-" + (IMAGENS[ext] ? "img" : ext);
172
+ t.textContent = ext.slice(0, 4);
173
+ var n = document.createElement("span");
174
+ n.className = "nome-aba";
175
+ n.textContent = nomeDe(a.caminho);
176
+ var x = document.createElement("span");
177
+ x.className = "fechar";
178
+ x.setAttribute("aria-label", "Fechar " + nomeDe(a.caminho));
179
+ x.innerHTML = "<span>×</span>";
180
+ x.onclick = function (ev) { ev.stopPropagation(); fecharAba(a.caminho); };
181
+ b.onclick = function () { irPara(a.caminho); };
182
+ b.onkeydown = function (ev) { if (ev.key === "Enter") irPara(a.caminho); };
183
+ b.appendChild(t); b.appendChild(n); b.appendChild(x);
184
+ rolo.appendChild(b);
185
+ });
186
+ }
187
+
188
+ /* Clique da pessoa num arquivo. Janela estreita com a prévia no palco
189
+ inteiro: clicar num arquivo é querer ver o arquivo, então a prévia sai
190
+ da frente. (Quando é o AGENTE que mexe, a prévia fica onde está.) */
191
+ function irPara(caminho) {
192
+ if (previaLigada && matchMedia("(max-width: 1240px)").matches) ligarPrevia(false);
193
+ return abrirArquivo(caminho);
194
+ }
195
+
196
+ function abrirArquivo(caminho) {
197
+ if (aba(caminho)) { mostrarAba(caminho); return Promise.resolve(); }
198
+ return pedir("/api/ide/arquivo?p=" + encodeURIComponent(caminho)).then(function (d) {
199
+ if (!d.ok) { avisar(d.error || "não abriu"); return; }
200
+ var a = { caminho: d.caminho, mtime: d.mtime, tamanho: d.tamanho, rolagem: 0, selecao: [0, 0] };
201
+ if (d.binario) a.tipo = "binario";
202
+ else if (d.grande) a.tipo = "grande";
203
+ else { a.tipo = "texto"; a.conteudo = a.original = d.conteudo; }
204
+ abertas.push(a);
205
+ mostrarAba(a.caminho);
206
+ });
207
+ }
208
+
209
+ function guardarPosicao() {
210
+ var a = aba(ativa);
211
+ if (!a || a.tipo !== "texto") return;
212
+ var c = $("codigo");
213
+ a.rolagem = c.scrollTop; a.rolagemX = c.scrollLeft;
214
+ a.selecao = [c.selectionStart, c.selectionEnd];
215
+ }
216
+
217
+ function mostrarAba(caminho) {
218
+ guardarPosicao();
219
+ ativa = caminho;
220
+ var a = aba(caminho);
221
+ $("boasVindas").hidden = !!a;
222
+ $("editor").hidden = !(a && a.tipo === "texto");
223
+ $("avisoArquivo").hidden = !(a && a.tipo !== "texto");
224
+ $("faixa").hidden = !(a && a.conflito);
225
+ if (a && a.conflito) $("faixaTexto").textContent = a.conflito;
226
+ if (a && a.tipo === "texto") {
227
+ var c = $("codigo");
228
+ c.value = a.conteudo;
229
+ c.setSelectionRange(a.selecao[0], a.selecao[1]);
230
+ c.scrollTop = a.rolagem; c.scrollLeft = a.rolagemX || 0;
231
+ desenharCalha(true);
232
+ c.focus();
233
+ } else if (a) {
234
+ var img = $("avisoImagem");
235
+ var ext = extensao(a.caminho);
236
+ img.hidden = !IMAGENS[ext];
237
+ if (IMAGENS[ext]) img.src = enderecoPrevia(a.caminho) + "?t=" + a.mtime;
238
+ $("avisoTexto").textContent = a.tipo === "grande"
239
+ ? nomeDe(a.caminho) + " tem " + tamanhoLegivel(a.tamanho) + ". O editor abre arquivos de até 1 MB — acima disso ele fica lento a cada tecla."
240
+ : IMAGENS[ext] ? nomeDe(a.caminho) + " · " + tamanhoLegivel(a.tamanho)
241
+ : nomeDe(a.caminho) + " não é texto (" + tamanhoLegivel(a.tamanho) + "), então não abre no editor. Peça ao Primo o que você quer fazer com ele.";
242
+ }
243
+ desenharAbas();
244
+ desenharArvore();
245
+ pintarAnexo();
246
+ pintarBarra();
247
+ }
248
+
249
+ function fecharAba(caminho) {
250
+ var a = aba(caminho);
251
+ if (suja(a) && !a.confirmouFechar) {
252
+ // Sem confirm(): a janela em modo app às vezes some com o diálogo. A
253
+ // primeira tentativa avisa; a segunda fecha.
254
+ a.confirmouFechar = true;
255
+ avisar(nomeDe(caminho) + " tem mudança não salva — Ctrl+S salva, ou feche de novo para descartar.");
256
+ return;
257
+ }
258
+ var i = abertas.indexOf(a);
259
+ abertas.splice(i, 1);
260
+ if (ativa === caminho) {
261
+ ativa = null;
262
+ var prox = abertas[i] || abertas[i - 1];
263
+ if (prox) { mostrarAba(prox.caminho); return; }
264
+ }
265
+ mostrarAba(ativa);
266
+ }
267
+
268
+ function fecharTudo() { abertas = []; ativa = null; mostrarAba(null); }
269
+
270
+ function tamanhoLegivel(n) {
271
+ if (n < 1024) return n + " bytes";
272
+ if (n < 1024 * 1024) return (n / 1024).toFixed(0) + " KB";
273
+ return (n / 1024 / 1024).toFixed(1).replace(".", ",") + " MB";
274
+ }
275
+
276
+ /* ── editor ─────────────────────────────────────────────────────────── */
277
+
278
+ var codigo = $("codigo");
279
+ var calha = $("calha");
280
+ var linhasNaCalha = 0;
281
+ var ALTURA = 20; // = --linha no ide.css
282
+
283
+ function desenharCalha(forcar) {
284
+ var n = codigo.value.split("\n").length;
285
+ if (forcar || n !== linhasNaCalha) {
286
+ var s = "";
287
+ for (var i = 1; i <= n; i++) s += i + "\n";
288
+ calha.textContent = s;
289
+ linhasNaCalha = n;
290
+ }
291
+ calha.scrollTop = codigo.scrollTop;
292
+ marcarLinhaAtual();
293
+ }
294
+
295
+ function posicao() {
296
+ var antes = codigo.value.slice(0, codigo.selectionStart);
297
+ var linha = antes.split("\n").length;
298
+ return { linha: linha, coluna: antes.length - antes.lastIndexOf("\n") };
299
+ }
300
+
301
+ function marcarLinhaAtual() {
302
+ var p = posicao();
303
+ $("linhaAtual").style.transform = "translateY(" + ((p.linha - 1) * ALTURA - codigo.scrollTop) + "px)";
304
+ var a = aba(ativa);
305
+ if (a && a.tipo === "texto") $("barraPosicao").textContent = "Ln " + p.linha + ", Col " + p.coluna;
306
+ }
307
+
308
+ /* Inserir por execCommand mantém o Ctrl+Z funcionando: mexer no `value`
309
+ direto apaga o histórico de desfazer do textarea inteiro. */
310
+ function inserir(texto) {
311
+ if (!document.execCommand("insertText", false, texto)) {
312
+ codigo.setRangeText(texto, codigo.selectionStart, codigo.selectionEnd, "end");
313
+ codigo.dispatchEvent(new Event("input"));
314
+ }
315
+ }
316
+
317
+ function indentar(menos) {
318
+ var v = codigo.value, ini = codigo.selectionStart, fim = codigo.selectionEnd;
319
+ if (!menos && ini === fim) { inserir(" "); return; }
320
+ var comeco = v.lastIndexOf("\n", ini - 1) + 1;
321
+ var bloco = v.slice(comeco, fim);
322
+ var novo = menos ? bloco.replace(/^ {1,2}|^\t/gm, "") : bloco.replace(/^/gm, " ");
323
+ codigo.setSelectionRange(comeco, fim);
324
+ inserir(novo);
325
+ codigo.setSelectionRange(comeco, comeco + novo.length);
326
+ }
327
+
328
+ codigo.addEventListener("keydown", function (ev) {
329
+ if (ev.key === "Tab") { ev.preventDefault(); indentar(ev.shiftKey); return; }
330
+ if (ev.key === "Enter" && !ev.ctrlKey && !ev.metaKey) {
331
+ // A linha nova começa na mesma indentação da de cima — sem isto, cada
332
+ // Enter em HTML aninhado joga o cursor de volta à coluna 1.
333
+ var v = codigo.value, ini = codigo.selectionStart;
334
+ var linha = v.slice(v.lastIndexOf("\n", ini - 1) + 1, ini);
335
+ var recuo = /^[ \t]*/.exec(linha)[0];
336
+ if (/[{[(>]\s*$/.test(linha) && !/<\/[^>]+>\s*$/.test(linha)) recuo += " ";
337
+ ev.preventDefault();
338
+ inserir("\n" + recuo);
339
+ }
340
+ });
341
+ codigo.addEventListener("input", function () {
342
+ var a = aba(ativa);
343
+ if (!a || a.tipo !== "texto") return;
344
+ var eraSuja = suja(a);
345
+ a.conteudo = codigo.value;
346
+ a.confirmouFechar = false;
347
+ desenharCalha(false);
348
+ if (eraSuja !== suja(a)) desenharAbas();
349
+ pintarBarra();
350
+ });
351
+ codigo.addEventListener("scroll", function () { calha.scrollTop = codigo.scrollTop; marcarLinhaAtual(); });
352
+ ["click", "keyup", "select"].forEach(function (e) { codigo.addEventListener(e, marcarLinhaAtual); });
353
+
354
+ function salvar(forcar) {
355
+ var a = aba(ativa);
356
+ if (!a || a.tipo !== "texto") return;
357
+ guardarPosicao();
358
+ pedir("/api/ide/salvar", { caminho: a.caminho, conteudo: a.conteudo, mtimeBase: a.mtime, forcar: !!forcar })
359
+ .then(function (d) {
360
+ if (d.conflito) {
361
+ a.conflito = nomeDe(a.caminho) + " mudou no disco depois que você abriu (provavelmente o Primo).";
362
+ mostrarAba(a.caminho);
363
+ return;
364
+ }
365
+ if (!d.ok) { avisar("Não salvou: " + (d.error || "erro")); return; }
366
+ a.original = a.conteudo; a.mtime = d.mtime; a.conflito = null;
367
+ $("faixa").hidden = true;
368
+ desenharAbas();
369
+ pintarBarra("salvo agora");
370
+ });
371
+ }
372
+
373
+ /* O agente mexeu num arquivo aberto. Se a pessoa não tinha mexido, o
374
+ editor só acompanha (guardando o cursor). Se tinha, ninguém perde nada
375
+ calado: a faixa pergunta qual das duas fica. */
376
+ function atualizarAberta(a) {
377
+ return pedir("/api/ide/arquivo?p=" + encodeURIComponent(a.caminho)).then(function (d) {
378
+ if (!d.ok) return;
379
+ if (d.mtime === a.mtime) return;
380
+ if (d.conteudo === undefined) { a.mtime = d.mtime; return; }
381
+ if (suja(a)) {
382
+ a.conflito = nomeDe(a.caminho) + " mudou no disco (provavelmente o Primo) e você tem mudanças não salvas.";
383
+ if (a.caminho === ativa) mostrarAba(a.caminho);
384
+ return;
385
+ }
386
+ if (a.caminho === ativa) guardarPosicao();
387
+ a.conteudo = a.original = d.conteudo; a.mtime = d.mtime;
388
+ if (a.caminho === ativa) mostrarAba(a.caminho);
389
+ });
390
+ }
391
+
392
+ $("faixaRecarregar").onclick = function () {
393
+ var a = aba(ativa); if (!a) return;
394
+ a.conflito = null;
395
+ pedir("/api/ide/arquivo?p=" + encodeURIComponent(a.caminho)).then(function (d) {
396
+ if (d.ok && d.conteudo !== undefined) { a.conteudo = a.original = d.conteudo; a.mtime = d.mtime; }
397
+ mostrarAba(a.caminho);
398
+ });
399
+ };
400
+ $("faixaManter").onclick = function () {
401
+ var a = aba(ativa); if (!a) return;
402
+ a.conflito = null;
403
+ // Manter a minha é gravar a minha: o disco passa a ser o que está na tela.
404
+ salvar(true);
405
+ $("faixa").hidden = true;
406
+ };
407
+
408
+ /* ── prévia ─────────────────────────────────────────────────────────── */
409
+
410
+ function enderecoPrevia(caminho) {
411
+ return "/api/ide/previa/" + CHAVE + "/" + String(caminho || "").split("/").map(encodeURIComponent).join("/");
412
+ }
413
+
414
+ function pintarPreviaVazia() {
415
+ var tem = !!projeto.entrada;
416
+ $("previa").hidden = !tem;
417
+ $("previaVazia").hidden = tem;
418
+ $("previaEndereco").textContent = projeto.entrada || "sem index.html";
419
+ }
420
+
421
+ function recarregarPrevia() {
422
+ if (!previaLigada || !projeto.entrada) return;
423
+ $("previa").src = enderecoPrevia(projeto.entrada) + "?t=" + Date.now();
424
+ var e = $("previaEstado");
425
+ e.textContent = "atualizada " + new Date().toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
426
+ e.classList.add("fresco");
427
+ setTimeout(function () { e.classList.remove("fresco"); }, 1600);
428
+ }
429
+
430
+ function ligarPrevia(sim) {
431
+ previaLigada = sim;
432
+ $("areaPrevia").hidden = !sim;
433
+ $("centro").classList.toggle("com-previa", sim);
434
+ $("btnPrevia").setAttribute("aria-pressed", sim ? "true" : "false");
435
+ if (sim) { pintarPreviaVazia(); recarregarPrevia(); }
436
+ }
437
+ $("btnPrevia").onclick = function () { ligarPrevia(!previaLigada); };
438
+ $("btnRecarregarPrevia").onclick = recarregarPrevia;
439
+
440
+ /* ── o que o vigia manda ────────────────────────────────────────────── */
441
+
442
+ var esperaArvore = null, esperaPrevia = null;
443
+
444
+ function aoMudarArquivos(ev) {
445
+ if (ev.trocou && projeto.pasta && ev.pasta !== projeto.pasta) { carregarArvore(); return; }
446
+ var deVoce = {};
447
+ (ev.deVoce || []).forEach(function (c) { deVoce[c] = true; });
448
+ var doAgente = (ev.mudados || []).filter(function (c) { return !deVoce[c]; });
449
+ var agora = Date.now();
450
+ doAgente.forEach(function (c) { mexidos[c] = agora; revelar(c); });
451
+ if (doAgente.length) {
452
+ avisar("Primo mexeu em " + nomeDe(doAgente[0]) + (doAgente.length > 1 ? " e mais " + (doAgente.length - 1) : ""));
453
+ }
454
+ abertas.forEach(function (a) { if (doAgente.indexOf(a.caminho) >= 0) atualizarAberta(a); });
455
+
456
+ clearTimeout(esperaArvore);
457
+ esperaArvore = setTimeout(carregarArvore, 200);
458
+ // A prévia espera um pouco mais: o agente costuma escrever o HTML, o
459
+ // CSS e o JS em sequência, e recarregar a cada um faz a página piscar.
460
+ clearTimeout(esperaPrevia);
461
+ esperaPrevia = setTimeout(function () {
462
+ if (!projeto.entrada) carregarArvore().then(recarregarPrevia);
463
+ else recarregarPrevia();
464
+ }, 450);
465
+ }
466
+
467
+ /* ── chat e terminal ────────────────────────────────────────────────── */
468
+
469
+ var tira = $("chatTira"), chatRolo = $("chatRolo"), term = $("terminalSaida");
470
+ var corChat = { cor: null, forte: false }, corTerm = { cor: null, forte: false };
471
+ var blocoChat = null;
472
+ var ocupado = false;
473
+
474
+ function noFim(el) { return el.scrollTop + el.clientHeight >= el.scrollHeight - 60; }
475
+
476
+ function escreverSaida(texto) {
477
+ var html = window.PrimoAnsi.paraHtml(texto, corTerm);
478
+ var fimT = noFim(term);
479
+ term.insertAdjacentHTML("beforeend", html);
480
+ // Teto do terminal: janela aberta o dia inteiro não pode crescer sem fim.
481
+ while (term.childNodes.length > 4000) term.removeChild(term.firstChild);
482
+ if (fimT) term.scrollTop = term.scrollHeight;
483
+
484
+ var fimC = noFim(chatRolo);
485
+ $("chatVazio").hidden = true;
486
+ if (!blocoChat) {
487
+ blocoChat = document.createElement("pre");
488
+ blocoChat.className = "saida";
489
+ tira.appendChild(blocoChat);
490
+ }
491
+ blocoChat.insertAdjacentHTML("beforeend", window.PrimoAnsi.paraHtml(texto, corChat));
492
+ if (fimC) chatRolo.scrollTop = chatRolo.scrollHeight;
493
+ }
494
+
495
+ function escreverVoce(texto) {
496
+ $("chatVazio").hidden = true;
497
+ var linha = document.createElement("div");
498
+ linha.className = "voce";
499
+ var caixa = document.createElement("div");
500
+ caixa.textContent = texto;
501
+ linha.appendChild(caixa);
502
+ tira.appendChild(linha);
503
+ blocoChat = null;
504
+ corChat = { cor: null, forte: false };
505
+ chatRolo.scrollTop = chatRolo.scrollHeight;
506
+ }
507
+
508
+ function escreverErro(texto) {
509
+ var d = document.createElement("div");
510
+ d.className = "aviso-erro";
511
+ d.textContent = texto;
512
+ tira.appendChild(d);
513
+ blocoChat = null;
514
+ }
515
+
516
+ function pintarEstado(e) {
517
+ if (!e) return;
518
+ $("modo").textContent = e.modo || "";
519
+ $("esforco").textContent = e.esforco || "";
520
+ document.title = (e.projeto ? e.projeto + " — " : "") + "PrimoCode IDE";
521
+ }
522
+
523
+ function pintarOcupado(v) {
524
+ ocupado = !!v;
525
+ document.body.classList.toggle("ocupado", ocupado);
526
+ $("parar").hidden = !ocupado;
527
+ $("mandar").hidden = ocupado;
528
+ $("barraAgenteTxt").textContent = ocupado ? "Primo trabalhando…" : "Primo pronto";
529
+ $("campo").placeholder = ocupado ? "pode ir escrevendo o próximo" : "Peça alguma coisa…";
530
+ }
531
+
532
+ function ligarFluxo() {
533
+ var fonte = new EventSource("/api/fluxo?k=" + encodeURIComponent(CHAVE));
534
+ fonte.addEventListener("saida", function (ev) { escreverSaida(JSON.parse(ev.data)); });
535
+ fonte.addEventListener("voce", function (ev) { escreverVoce(JSON.parse(ev.data)); });
536
+ fonte.addEventListener("erro", function (ev) { escreverErro(JSON.parse(ev.data)); });
537
+ fonte.addEventListener("estado", function (ev) { pintarEstado(JSON.parse(ev.data)); });
538
+ fonte.addEventListener("ocupado", function (ev) { pintarOcupado(JSON.parse(ev.data)); });
539
+ fonte.addEventListener("arquivos", function (ev) { aoMudarArquivos(JSON.parse(ev.data)); });
540
+ }
541
+
542
+ /* "incluir index.html": o arquivo aberto vai junto no pedido como
543
+ @menção, o mesmo mecanismo do terminal (lib/mencoes.js). Pedir "melhora
544
+ isso aqui" com o arquivo na tela e o agente sem saber qual é seria a
545
+ primeira frustração de quem usa uma IDE. */
546
+ function pintarAnexo() {
547
+ var a = aba(ativa);
548
+ $("anexo").hidden = !a;
549
+ if (a) $("anexoNome").textContent = nomeDe(a.caminho);
550
+ }
551
+
552
+ function mencao(caminho) { return /\s/.test(caminho) ? '@"' + caminho + '"' : "@" + caminho; }
553
+
554
+ var campo = $("campo");
555
+ function enviar(texto) {
556
+ texto = String(texto || "").trim();
557
+ if (!texto) return;
558
+ var a = aba(ativa);
559
+ if (a && $("anexar").checked && texto.charAt(0) !== "/" && texto.indexOf(mencao(a.caminho)) < 0) {
560
+ texto += " (arquivo aberto: " + mencao(a.caminho) + ")";
561
+ }
562
+ pedir("/api/enviar", { texto: texto });
563
+ campo.value = "";
564
+ ajustarCampo();
565
+ }
566
+ function ajustarCampo() {
567
+ campo.style.height = "auto";
568
+ campo.style.height = Math.min(campo.scrollHeight, 180) + "px";
569
+ }
570
+ $("forma").addEventListener("submit", function (ev) { ev.preventDefault(); enviar(campo.value); });
571
+ campo.addEventListener("input", ajustarCampo);
572
+ campo.addEventListener("keydown", function (ev) {
573
+ if (ev.key === "Enter" && !ev.shiftKey) { ev.preventDefault(); enviar(campo.value); }
574
+ });
575
+ $("parar").onclick = function () { pedir("/api/parar", {}); };
576
+ document.querySelectorAll("[data-manda]").forEach(function (b) {
577
+ b.addEventListener("click", function () { enviar(b.dataset.manda); });
578
+ });
579
+
580
+ /* ── barra, terminal, atalhos ───────────────────────────────────────── */
581
+
582
+ var esperaAviso = null;
583
+ function avisar(texto) {
584
+ var el = $("barraAviso");
585
+ el.textContent = texto;
586
+ clearTimeout(esperaAviso);
587
+ esperaAviso = setTimeout(function () { el.textContent = ""; }, 6000);
588
+ }
589
+
590
+ function pintarBarra(salvoTexto) {
591
+ var a = aba(ativa);
592
+ var texto = !!(a && a.tipo === "texto");
593
+ $("barraLingua").textContent = texto ? (LINGUAS[extensao(a.caminho)] || "Texto") : "";
594
+ if (!texto) $("barraPosicao").textContent = "";
595
+ var s = $("barraSalvo");
596
+ s.textContent = !texto ? "" : suja(a) ? "● não salvo" : (salvoTexto || "salvo");
597
+ s.className = "barra-item barra-salvo" + (texto && suja(a) ? " sujo" : "");
598
+ }
599
+
600
+ function mostrarTerminal(sim) {
601
+ document.body.classList.toggle("sem-terminal", !sim);
602
+ $("btnTerminal").setAttribute("aria-pressed", sim ? "true" : "false");
603
+ try { localStorage.setItem("primocode_ide_terminal", sim ? "1" : "0"); } catch (e) {}
604
+ if (sim) term.scrollTop = term.scrollHeight;
605
+ }
606
+ $("btnTerminal").onclick = function () { mostrarTerminal(document.body.classList.contains("sem-terminal")); };
607
+ $("btnFecharTerminal").onclick = function () { mostrarTerminal(false); };
608
+ $("btnRecarregar").onclick = carregarArvore;
609
+ // "Só o chat" leva a chave junto: a outra página a lê da URL.
610
+ $("linkChat").href = "/?k=" + encodeURIComponent(CHAVE);
611
+
612
+ document.addEventListener("keydown", function (ev) {
613
+ var mod = ev.ctrlKey || ev.metaKey;
614
+ if (mod && ev.key.toLowerCase() === "s") { ev.preventDefault(); salvar(false); return; }
615
+ if (mod && ev.key.toLowerCase() === "j") { ev.preventDefault(); $("btnTerminal").click(); return; }
616
+ if (ev.key === "Escape" && ocupado) pedir("/api/parar", {});
617
+ });
618
+ // Fechar a janela com arquivo não salvo: o único aviso que o navegador
619
+ // deixa dar nessa hora.
620
+ window.addEventListener("beforeunload", function (ev) {
621
+ if (abertas.some(suja)) { ev.preventDefault(); ev.returnValue = ""; }
622
+ });
623
+
624
+ /* ── abertura ───────────────────────────────────────────────────────────
625
+ A IDE abre trabalhando, não vazia: se o projeto tem um index.html, ele
626
+ já vem aberto no editor e a prévia ao lado. */
627
+ var terminalGuardado = "1";
628
+ try { terminalGuardado = localStorage.getItem("primocode_ide_terminal") || "1"; } catch (e) {}
629
+ mostrarTerminal(terminalGuardado !== "0");
630
+ pintarOcupado(false);
631
+ pedir("/api/estado").then(pintarEstado).catch(function () {});
632
+ carregarArvore().then(function () {
633
+ if (projeto.entrada) {
634
+ revelar(projeto.entrada);
635
+ abrirArquivo(projeto.entrada).then(function () { ligarPrevia(true); });
636
+ }
637
+ });
638
+ ligarFluxo();
639
+ })();