primocode 9.9.0 → 10.0.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.
Files changed (63) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +100 -341
  3. package/lib/act.js +245 -23
  4. package/lib/browser.js +17 -0
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/config.js +10 -3
  8. package/lib/cursor.js +10 -1
  9. package/lib/entrada.js +4 -2
  10. package/lib/estudio/diretor.js +9 -1
  11. package/lib/estudio/ende.js +2 -0
  12. package/lib/estudio/estilo.js +51 -0
  13. package/lib/estudio/glifos.js +215 -0
  14. package/lib/estudio/index.js +63 -11
  15. package/lib/estudio/kit/base.jsx +285 -0
  16. package/lib/estudio/kit/index.jsx +11 -0
  17. package/lib/estudio/kit/marca.jsx +741 -0
  18. package/lib/estudio/kit/vox.jsx +357 -0
  19. package/lib/estudio/kit.js +72 -0
  20. package/lib/estudio/lancamento.js +413 -0
  21. package/lib/estudio/morfar.js +191 -0
  22. package/lib/estudio/motion.js +333 -0
  23. package/lib/estudio/plano.js +54 -7
  24. package/lib/estudio/remotion.js +167 -52
  25. package/lib/estudio/ritmo.js +77 -0
  26. package/lib/estudio/sons-leves.js +62 -0
  27. package/lib/estudio/vox.js +385 -0
  28. package/lib/fala.js +1 -94
  29. package/lib/pipeline.js +55 -56
  30. package/lib/repeticao.js +3 -1
  31. package/lib/skills.js +8 -2
  32. package/lib/snapshot.js +111 -0
  33. package/lib/tools.js +79 -20
  34. package/lib/ui.js +13 -6
  35. package/lib/video/analise.js +89 -0
  36. package/lib/video/cli.js +74 -0
  37. package/lib/video/delta.js +90 -0
  38. package/lib/video/edits.js +379 -0
  39. package/lib/video/filters.js +247 -0
  40. package/lib/video/ops.js +322 -0
  41. package/lib/video/projeto.js +133 -0
  42. package/lib/video/render.js +221 -0
  43. package/lib/video/texto.js +142 -0
  44. package/lib/video/timeline.js +310 -0
  45. package/lib/video/transcript.js +124 -0
  46. package/lib/voz.js +75 -223
  47. package/package.json +2 -3
  48. package/skills/motion-de-primeira/SKILL.md +21 -1
  49. package/studio/texto.py +208 -0
  50. package/voz/primo_voz/__main__.py +96 -236
  51. package/voz/primo_voz/transcrever.py +31 -1
  52. package/app/ansi.js +0 -55
  53. package/app/app.css +0 -500
  54. package/app/app.js +0 -1017
  55. package/app/icone.svg +0 -6
  56. package/app/index.html +0 -220
  57. package/app/primo-marca.js +0 -192
  58. package/lib/app.js +0 -615
  59. package/lib/icone.js +0 -263
  60. package/lib/instalar-app.js +0 -231
  61. package/lib/painel.js +0 -291
  62. package/voz/primo_voz/desperta.py +0 -195
  63. package/voz/primo_voz/orbe.py +0 -406
package/app/app.js DELETED
@@ -1,1017 +0,0 @@
1
- /* A janela do PrimoCode conversando com o agente.
2
- *
3
- * Nada de framework: é a mesma regra do resto do projeto (zero dependência),
4
- * e aqui ela nem custa — a página tem uma barra lateral, uma conversa, um
5
- * compositor e um painel, ligados por um fluxo de eventos (SSE).
6
- *
7
- * A conversa chega ESTRUTURADA (evento `agente`, vindo de lib/fala.js): a
8
- * fala do Primo vira texto, cada ferramenta vira uma linha cinza e o
9
- * resultado dela fecha essa linha. O terminal cru continua chegando
10
- * (`saida`) e mora atrás do botão "Terminal".
11
- */
12
- (function () {
13
- "use strict";
14
-
15
- /* A chave chega na URL na primeira abertura. Sai de lá assim que é lida —
16
- numa aba comum ficaria à mostra no histórico — mas antes vai para o
17
- sessionStorage: sem isso, um F5 matava a janela. */
18
- var busca = new URLSearchParams(location.search);
19
- var CHAVE = busca.get("k") || "";
20
- var ABRIR_PAINEL = busca.get("painel") || "";
21
- try {
22
- if (CHAVE) sessionStorage.setItem("primo-chave", CHAVE);
23
- else CHAVE = sessionStorage.getItem("primo-chave") || "";
24
- } catch (e) { /* sem sessionStorage: vale só esta abertura */ }
25
- if (location.search) history.replaceState(null, "", location.pathname);
26
-
27
- var $ = function (id) { return document.getElementById(id); };
28
- var app = $("app");
29
- var rolo = $("rolo");
30
- var chat = $("chat");
31
- var vazio = $("vazio");
32
- var campo = $("campo");
33
- var forma = $("forma");
34
- var btnEnviar = $("btnEnviar");
35
- var estado = {};
36
- var ocupado = false;
37
-
38
- var pego = function (k, p) { try { return localStorage.getItem(k) || p; } catch (e) { return p; } };
39
- var guardo = function (k, v) { try { localStorage.setItem(k, v); } catch (e) {} };
40
-
41
- function pedir(rota, corpo) {
42
- return fetch(rota, {
43
- method: corpo === undefined ? "GET" : "POST",
44
- headers: { "Content-Type": "application/json", "X-Primo-Chave": CHAVE },
45
- body: corpo === undefined ? undefined : JSON.stringify(corpo),
46
- }).then(function (r) {
47
- return r.json().catch(function () { return { ok: false, error: "resposta ilegível" }; })
48
- .then(function (d) { d = d || {}; d.__status = r.status; return d; });
49
- });
50
- }
51
-
52
- function el(tag, classe, texto) {
53
- var e = document.createElement(tag);
54
- if (classe) e.className = classe;
55
- if (texto !== undefined) e.textContent = texto;
56
- return e;
57
- }
58
- function icone(nome, classe) {
59
- var ns = "http://www.w3.org/2000/svg";
60
- var s = document.createElementNS(ns, "svg");
61
- s.setAttribute("class", "ic" + (classe ? " " + classe : ""));
62
- s.setAttribute("aria-hidden", "true");
63
- var u = document.createElementNS(ns, "use");
64
- u.setAttribute("href", "#i-" + nome);
65
- s.appendChild(u);
66
- return s;
67
- }
68
- function fugir(s) {
69
- return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
70
- }
71
-
72
- /* ── os rótulos que a pessoa lê ──────────────────────────────────────
73
- Os modos e os níveis são os do CLI (lib/perigo.js, lib/effort.js); os
74
- nomes aqui são os que quem não programa entende. */
75
- var PERMISSOES = {
76
- auto: { rotulo: "Pergunta antes do perigoso", icone: "escudo",
77
- desc: "Faz o trabalho e só pede licença antes do que não tem volta" },
78
- perguntar: { rotulo: "Pergunta sempre", icone: "mao",
79
- desc: "Pede licença antes de cada comando e de cada arquivo" },
80
- livre: { rotulo: "Acesso completo", icone: "perigo",
81
- desc: "Nunca pergunta, nem antes de apagar ou forçar push" },
82
- };
83
- var ESFORCOS = [
84
- { nome: "low", rotulo: "Rápido", desc: "tarefa simples, resposta curta" },
85
- { nome: "medium", rotulo: "Equilíbrio", desc: "o padrão do dia a dia" },
86
- { nome: "high", rotulo: "Capricho", desc: "capricha e pesquisa referências" },
87
- { nome: "xhigh", rotulo: "Esperto", desc: "o mais capaz, contexto enorme" },
88
- { nome: "max", rotulo: "Máximo", desc: "pesquisa a fundo antes de escrever" },
89
- { nome: "ultracode", rotulo: "Ultra", desc: "tudo no talo" },
90
- ];
91
- function esforcoDe(nome) {
92
- for (var i = 0; i < ESFORCOS.length; i++) if (ESFORCOS[i].nome === nome) return ESFORCOS[i];
93
- return ESFORCOS[1];
94
- }
95
-
96
- /* ── o menu flutuante ────────────────────────────────────────────────
97
- Um só, reaproveitado por todos os botões com ⌄: projeto, permissão,
98
- esforço e o nome do app. Abre para cima quando o botão está embaixo —
99
- o compositor fica colado no rodapé, e um menu para baixo sairia da tela. */
100
- var menu = $("menu");
101
- var menuDono = null;
102
-
103
- function fecharMenu() {
104
- menu.hidden = true;
105
- menu.innerHTML = "";
106
- if (menuDono) menuDono.setAttribute("aria-expanded", "false");
107
- menuDono = null;
108
- }
109
-
110
- function abrirMenu(dono, itens) {
111
- if (menuDono === dono) { fecharMenu(); return; }
112
- fecharMenu();
113
- menuDono = dono;
114
- dono.setAttribute("aria-expanded", "true");
115
- itens.forEach(function (it) {
116
- if (it.sep) { menu.appendChild(el("div", "menu-sep")); return; }
117
- if (it.titulo) { menu.appendChild(el("div", "menu-titulo", it.titulo)); return; }
118
- if (it.entrada) {
119
- var f = el("form", "menu-entrada");
120
- var i = el("input");
121
- i.id = it.entrada.id; i.placeholder = it.entrada.placeholder; i.maxLength = 60;
122
- var b = el("button", "", it.entrada.botao);
123
- b.type = "submit";
124
- f.appendChild(i); f.appendChild(b);
125
- f.onsubmit = function (ev) {
126
- ev.preventDefault();
127
- if (!i.value.trim()) return;
128
- fecharMenu();
129
- it.entrada.acao(i.value.trim());
130
- };
131
- menu.appendChild(f);
132
- setTimeout(function () { i.focus(); }, 0);
133
- return;
134
- }
135
- var botao = el("button", "menu-item" + (it.classe ? " " + it.classe : ""));
136
- botao.type = "button";
137
- botao.setAttribute("role", "menuitem");
138
- if (it.icone) botao.appendChild(icone(it.icone));
139
- var rot = el("span", "rot", it.rotulo);
140
- if (it.desc) rot.appendChild(el("span", "", it.desc));
141
- botao.appendChild(rot);
142
- if (it.marcado) botao.appendChild(el("span", "marcado", "✓"));
143
- botao.onclick = function () { fecharMenu(); it.acao(); };
144
- menu.appendChild(botao);
145
- });
146
- menu.hidden = false;
147
- var r = dono.getBoundingClientRect();
148
- var m = menu.getBoundingClientRect();
149
- var esq = Math.min(r.left, window.innerWidth - m.width - 8);
150
- var embaixo = r.top > window.innerHeight / 2;
151
- menu.style.left = Math.max(8, esq) + "px";
152
- menu.style.top = (embaixo ? Math.max(8, r.top - m.height - 6) : r.bottom + 6) + "px";
153
- var primeiro = menu.querySelector("button, input");
154
- if (primeiro && !itens.some(function (x) { return x.entrada; })) primeiro.focus();
155
- }
156
-
157
- document.addEventListener("mousedown", function (ev) {
158
- if (!menu.hidden && !menu.contains(ev.target) && menuDono && !menuDono.contains(ev.target)) fecharMenu();
159
- });
160
-
161
- /* ── a conversa ──────────────────────────────────────────────────────── */
162
-
163
- function noFim() { return rolo.scrollTop + rolo.clientHeight >= rolo.scrollHeight - 80; }
164
- // Só acompanha se a pessoa JÁ estava embaixo: puxar a tela enquanto
165
- // alguém lê o que passou é a coisa mais irritante que um chat ao vivo faz.
166
- function acompanhar(estava) { if (estava) rolo.scrollTop = rolo.scrollHeight; }
167
-
168
- function mostrarConversa() { vazio.hidden = true; chat.hidden = false; }
169
- function limparConversa() {
170
- chat.innerHTML = "";
171
- chat.hidden = true;
172
- vazio.hidden = false;
173
- fala = null; grupo = null; pendentes = [];
174
- trabalhando = null;
175
- if (ocupado) mostrarTrabalhando();
176
- }
177
-
178
- /* Markdown pequeno e SEGURO: tudo é escapado antes, e só depois o que é
179
- marcação vira tag. A resposta vem do modelo, e o modelo leu páginas da
180
- internet — um <script> que ele repetisse não pode virar script aqui. */
181
- function markdown(texto) {
182
- var blocos = [];
183
- var t = String(texto || "").replace(/\r/g, "");
184
- t = t.replace(/```[^\n]*\n([\s\S]*?)(```|$)/g, function (_, codigo) {
185
- blocos.push("<pre><code>" + fugir(codigo.replace(/\n$/, "")) + "</code></pre>");
186
- return "\u0000" + (blocos.length - 1) + "\u0000";
187
- });
188
- var linhas = t.split("\n");
189
- var fora = [];
190
- var lista = null;
191
- var para = [];
192
- function inline(s) {
193
- return fugir(s)
194
- .replace(/`([^`]+)`/g, "<code>$1</code>")
195
- .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
196
- .replace(/(^|[\s(])\*([^*\s][^*]*)\*/g, "$1<em>$2</em>");
197
- }
198
- function fecharPara() { if (para.length) { fora.push("<p>" + para.map(inline).join("<br>") + "</p>"); para = []; } }
199
- function fecharLista() { if (lista) { fora.push("</" + lista + ">"); lista = null; } }
200
- linhas.forEach(function (l) {
201
- var bloco = /^\u0000(\d+)\u0000$/.exec(l.trim());
202
- var item = /^\s*[-*•]\s+(.*)$/.exec(l);
203
- var num = /^\s*\d+[.)]\s+(.*)$/.exec(l);
204
- var tit = /^\s*#{1,6}\s+(.*)$/.exec(l);
205
- if (bloco) { fecharPara(); fecharLista(); fora.push(blocos[Number(bloco[1])]); return; }
206
- if (item || num) {
207
- fecharPara();
208
- var tipo = item ? "ul" : "ol";
209
- if (lista !== tipo) { fecharLista(); fora.push("<" + tipo + ">"); lista = tipo; }
210
- fora.push("<li>" + inline((item || num)[1]) + "</li>");
211
- return;
212
- }
213
- if (tit) { fecharPara(); fecharLista(); fora.push("<h4>" + inline(tit[1]) + "</h4>"); return; }
214
- if (!l.trim()) { fecharPara(); fecharLista(); return; }
215
- fecharLista();
216
- para.push(l);
217
- });
218
- fecharPara(); fecharLista();
219
- return fora.join("").replace(/\u0000(\d+)\u0000/g, function (_, i) { return blocos[Number(i)]; });
220
- }
221
-
222
- var fala = null; // o bloco de texto do Primo que está chegando aos pedaços
223
- var grupo = null; // o grupo de linhas de ação em andamento
224
- var pendentes = []; // ações esperando o resultado
225
- var trabalhando = null; // o "Trabalhando…" do fim da conversa
226
-
227
- function anexar(no) {
228
- mostrarConversa();
229
- if (trabalhando && trabalhando.parentNode === chat) chat.insertBefore(no, trabalhando);
230
- else chat.appendChild(no);
231
- }
232
-
233
- function mostrarTrabalhando() {
234
- if (trabalhando) return;
235
- trabalhando = el("div", "trabalhando");
236
- trabalhando.appendChild(el("span", "pulso"));
237
- trabalhando.appendChild(el("span", "brilho", "Trabalhando"));
238
- mostrarConversa();
239
- chat.appendChild(trabalhando);
240
- }
241
- function esconderTrabalhando() {
242
- if (trabalhando && trabalhando.parentNode) trabalhando.parentNode.removeChild(trabalhando);
243
- trabalhando = null;
244
- // O que ficou sem resultado (parou no meio) não pode girar para sempre.
245
- pendentes.forEach(function (p) { var r = p.querySelector(".rodando"); if (r) r.remove(); });
246
- pendentes = [];
247
- }
248
-
249
- function escreverVoce(texto) {
250
- fala = null; grupo = null;
251
- var linha = el("div", "msg-voce");
252
- linha.appendChild(el("div", "balao", texto));
253
- anexar(linha);
254
- rolo.scrollTop = rolo.scrollHeight;
255
- }
256
-
257
- function blocoDeFala() {
258
- if (!fala) {
259
- grupo = null;
260
- fala = el("div", "msg-primo");
261
- fala.__texto = "";
262
- anexar(fala);
263
- }
264
- return fala;
265
- }
266
-
267
- var ICONES = { ler: "ler", escrever: "arquivo", editar: "editar", rodar: "rodar", buscar: "busca",
268
- web: "web", computador: "computador", plano: "plano", agente: "agente", outro: "barra" };
269
-
270
- function linhaDeAcao(e) {
271
- var estava = noFim();
272
- fala = null;
273
- if (!grupo) { grupo = el("div", "acoes"); anexar(grupo); }
274
- var caixa = el("div", "acao" + (e.prof ? " fundo" : ""));
275
- var linha = el("button", "acao-linha");
276
- linha.type = "button";
277
- linha.appendChild(icone(ICONES[e.icone] || "barra"));
278
- var texto = el("span", "acao-texto");
279
- texto.appendChild(el("b", "", e.verbo || e.nome || "Ação"));
280
- if (e.alvo) texto.appendChild(document.createTextNode(" " + e.alvo));
281
- linha.appendChild(texto);
282
- var meta = el("span", "acao-meta");
283
- meta.appendChild(el("span", "rodando"));
284
- linha.appendChild(meta);
285
- linha.appendChild(icone("chev", "acao-seta"));
286
- caixa.appendChild(linha);
287
- caixa.__meta = meta;
288
- caixa.__linha = linha;
289
- caixa.__texto = texto;
290
- caixa.__nome = e.nome;
291
- grupo.appendChild(caixa);
292
- pendentes.push(caixa);
293
- acompanhar(estava);
294
- }
295
-
296
- function fecharAcao(r) {
297
- var caixa = pendentes.pop();
298
- if (!caixa) return;
299
- var meta = caixa.__meta;
300
- meta.innerHTML = "";
301
- if (r.nome === "write_file" && r.criado) caixa.__texto.firstChild.textContent = "Criou";
302
- if (r.ok === false) {
303
- meta.appendChild(el("span", "falhou", "✗ " + (r.resumo || "falhou").slice(0, 80)));
304
- if (r.resumo && r.resumo.length > 80) r.detalhe = r.resumo;
305
- } else {
306
- if (r.mais || r.menos) {
307
- meta.appendChild(el("span", "mais", "+" + (r.mais || 0)));
308
- meta.appendChild(el("span", "menos", "−" + (r.menos || 0)));
309
- } else if (r.resumo) {
310
- meta.appendChild(el("span", "", r.resumo));
311
- }
312
- if (r.codigo !== undefined) meta.appendChild(el("span", r.codigo === 0 ? "ok" : "falhou", r.codigo === 0 ? "✓" : "exit " + r.codigo));
313
- }
314
- if (r.detalhe) {
315
- var linha = caixa.__linha;
316
- linha.classList.add("abre");
317
- linha.setAttribute("aria-expanded", "false");
318
- linha.onclick = function () {
319
- var aberta = caixa.classList.toggle("aberta");
320
- linha.setAttribute("aria-expanded", aberta ? "true" : "false");
321
- var d = caixa.querySelector(".acao-detalhe");
322
- if (aberta && !d) {
323
- d = el("pre", "acao-detalhe");
324
- String(r.detalhe).split("\n").forEach(function (l, i) {
325
- var s = el("span", l.charAt(0) === "+" && caixa.__nome !== "run_shell" ? "l-mais"
326
- : l.charAt(0) === "-" && caixa.__nome !== "run_shell" ? "l-menos" : "", (i ? "\n" : "") + l);
327
- d.appendChild(s);
328
- });
329
- caixa.appendChild(d);
330
- } else if (d) d.hidden = !aberta;
331
- };
332
- }
333
- }
334
-
335
- var agendado = null;
336
- function redesenharFala() {
337
- if (agendado) return;
338
- agendado = requestAnimationFrame(function () {
339
- agendado = null;
340
- if (fala) { var estava = noFim(); fala.innerHTML = markdown(fala.__texto); acompanhar(estava); }
341
- });
342
- }
343
-
344
- function aoAgente(e) {
345
- var estava = noFim();
346
- switch (e.tipo) {
347
- case "pedaco": {
348
- var b = blocoDeFala();
349
- b.__texto += e.texto;
350
- redesenharFala();
351
- return;
352
- }
353
- case "fim":
354
- if (fala) fala.innerHTML = markdown(fala.__texto);
355
- fala = null;
356
- break;
357
- case "resposta": {
358
- fala = null;
359
- var r = blocoDeFala();
360
- r.__texto = e.texto;
361
- r.innerHTML = markdown(e.texto);
362
- fala = null;
363
- break;
364
- }
365
- case "pensamento":
366
- fala = null; grupo = null;
367
- anexar(el("div", "pensamento", e.texto));
368
- break;
369
- case "aviso":
370
- fala = null; grupo = null;
371
- anexar(el("div", "aviso-msg", e.texto));
372
- break;
373
- case "acao": linhaDeAcao(e); return;
374
- case "resultado": fecharAcao(e); return;
375
- }
376
- acompanhar(estava);
377
- }
378
-
379
- function escreverComando(texto) {
380
- fala = null; grupo = null;
381
- var pre = el("pre", "saida-comando");
382
- pre.innerHTML = window.PrimoAnsi.paraHtml(String(texto).replace(/^\n+|\n+$/g, ""), { cor: null, forte: false });
383
- anexar(pre);
384
- acompanhar(true);
385
- }
386
-
387
- function escreverErro(texto) {
388
- fala = null; grupo = null;
389
- anexar(el("div", "aviso-msg erro", texto));
390
- acompanhar(true);
391
- }
392
-
393
- function reproduzir(lista) {
394
- chat.innerHTML = "";
395
- fala = null; grupo = null; pendentes = []; trabalhando = null;
396
- if (!lista || !lista.length) { limparConversa(); return; }
397
- lista.forEach(function (x) { tratar(x.evento, x.dados); });
398
- // Na história, ação sem resultado já passou: nada fica girando.
399
- pendentes.forEach(function (p) { var r = p.querySelector(".rodando"); if (r) r.remove(); });
400
- pendentes = [];
401
- if (ocupado) mostrarTrabalhando();
402
- rolo.scrollTop = rolo.scrollHeight;
403
- }
404
-
405
- function tratar(evento, dados) {
406
- if (evento === "voce") escreverVoce(dados);
407
- else if (evento === "agente") aoAgente(dados);
408
- else if (evento === "comando") escreverComando(dados);
409
- else if (evento === "erro") escreverErro(dados);
410
- }
411
-
412
- /* ── o terminal ──────────────────────────────────────────────────────── */
413
-
414
- var terminal = $("terminal");
415
- var terminalSaida = $("terminalSaida");
416
- var corTerminal = { cor: null, forte: false };
417
- function escreverTerminal(texto) {
418
- var estava = terminalSaida.scrollTop + terminalSaida.clientHeight >= terminalSaida.scrollHeight - 40;
419
- terminalSaida.insertAdjacentHTML("beforeend", window.PrimoAnsi.paraHtml(texto, corTerminal));
420
- // O terminal só guarda o fim: centenas de milhares de linhas deixariam
421
- // a janela lenta sem ninguém estar olhando.
422
- while (terminalSaida.childNodes.length > 6000) terminalSaida.removeChild(terminalSaida.firstChild);
423
- if (estava) terminalSaida.scrollTop = terminalSaida.scrollHeight;
424
- }
425
- function alternarTerminal(mostrar) {
426
- if (mostrar === undefined) mostrar = terminal.hidden;
427
- terminal.hidden = !mostrar;
428
- rolo.hidden = mostrar;
429
- $("navTerminal").setAttribute("aria-pressed", mostrar ? "true" : "false");
430
- if (mostrar) terminalSaida.scrollTop = terminalSaida.scrollHeight;
431
- else campo.focus();
432
- }
433
- $("navTerminal").onclick = function () { alternarTerminal(); };
434
- $("fecharTerminal").onclick = function () { alternarTerminal(false); };
435
-
436
- /* ── o estado ────────────────────────────────────────────────────────── */
437
-
438
- function iniciais(nome) {
439
- var p = String(nome || "").trim().split(/\s+/).filter(Boolean);
440
- if (!p.length) return "PC";
441
- return ((p[0][0] || "") + (p.length > 1 ? p[p.length - 1][0] : "")).toUpperCase();
442
- }
443
-
444
- function pintarEstado(e) {
445
- if (!e) return;
446
- estado = e;
447
- var projeto = e.projeto || "";
448
- $("projetoNome").textContent = projeto || "Escolher projeto";
449
- $("btnProjeto").classList.toggle("tem", !!projeto);
450
- $("topoTitulo").textContent = projeto;
451
- document.title = (projeto ? projeto + " · " : "") + "PrimoCode";
452
-
453
- var modo = PERMISSOES[e.permissoes] ? e.permissoes : "auto";
454
- var perm = $("btnPermissao");
455
- perm.dataset.modo = modo;
456
- $("permRotulo").textContent = PERMISSOES[modo].rotulo;
457
- $("permIcone").firstChild.setAttribute("href", "#i-" + PERMISSOES[modo].icone);
458
- perm.title = PERMISSOES[modo].desc + " (/permissoes)";
459
-
460
- var esf = esforcoDe(e.esforco);
461
- $("esforcoRotulo").textContent = esf.rotulo;
462
- $("esforcoNome").textContent = esf.nome;
463
-
464
- var mic = $("btnMic");
465
- mic.setAttribute("aria-pressed", e.voz ? "true" : "false");
466
- mic.setAttribute("aria-label", e.voz ? "Desligar o modo voz" : "Ligar o modo voz");
467
-
468
- $("nomeUsuario").textContent = e.usuario || "Você";
469
- $("avatar").textContent = iniciais(e.usuario || "Primo Code");
470
-
471
- if (e.mascote !== pintarEstado.mascote && window.PrimoMarca) {
472
- pintarEstado.mascote = e.mascote;
473
- $("mascote").innerHTML = window.PrimoMarca.linha({ mascote: e.mascote, tamanho: 56 });
474
- $("mascotePainel").innerHTML = window.PrimoMarca.linha({ mascote: e.mascote, tamanho: 40 });
475
- }
476
-
477
- var caixa = $("anexos");
478
- caixa.innerHTML = "";
479
- (e.anexos || []).forEach(function (nome) {
480
- var a = el("span", "anexo");
481
- a.appendChild(icone("arquivo"));
482
- a.appendChild(el("span", "", nome));
483
- var x = el("button");
484
- x.type = "button";
485
- x.setAttribute("aria-label", "Tirar " + nome);
486
- x.appendChild(icone("fechar"));
487
- x.onclick = function () { pedir("/api/desanexar", { nome: nome }).then(function (d) { if (d.estado) pintarEstado(d.estado); }); };
488
- a.appendChild(x);
489
- caixa.appendChild(a);
490
- });
491
- caixa.hidden = !(e.anexos || []).length;
492
- if (e.ocupado !== undefined) pintarOcupado(e.ocupado);
493
- marcarProjetoAtual();
494
- }
495
-
496
- function atualizarEnviar() {
497
- if (ocupado) {
498
- btnEnviar.disabled = false;
499
- btnEnviar.classList.add("parar");
500
- btnEnviar.setAttribute("aria-label", "Parar");
501
- btnEnviar.title = "Parar (Esc Esc)";
502
- btnEnviar.firstChild.firstChild.setAttribute("href", "#i-pare");
503
- } else {
504
- btnEnviar.classList.remove("parar");
505
- btnEnviar.disabled = !campo.value.trim();
506
- btnEnviar.setAttribute("aria-label", "Enviar");
507
- btnEnviar.title = "Enviar (Enter)";
508
- btnEnviar.firstChild.firstChild.setAttribute("href", "#i-seta");
509
- }
510
- }
511
-
512
- var avisar = pego("primo-avisar", "") === "1";
513
- function pintarOcupado(v) {
514
- var era = ocupado;
515
- ocupado = !!v;
516
- if (ocupado) mostrarTrabalhando(); else esconderTrabalhando();
517
- campo.placeholder = ocupado ? "Ele está trabalhando. Pode ir escrevendo o próximo" : "Peça qualquer coisa";
518
- atualizarEnviar();
519
- // Avisa só quando a janela está escondida: com ela na frente, a pessoa já viu.
520
- if (era && !ocupado && avisar && document.hidden && window.Notification && Notification.permission === "granted") {
521
- try { new Notification("PrimoCode", { body: "Terminei. Volte para ver o que eu fiz." }); } catch (e) {}
522
- }
523
- if (era && !ocupado) { carregarRecentes(); carregarProjetos(); }
524
- }
525
-
526
- /* ── o fluxo ─────────────────────────────────────────────────────────
527
- EventSource reconecta sozinho, e o servidor manda a história ao ligar —
528
- uma queda de rede local não deixa buraco na conversa. */
529
- function ligar() {
530
- var fonte = new EventSource("/api/fluxo?k=" + encodeURIComponent(CHAVE));
531
- var ouvir = function (nome, fn) {
532
- fonte.addEventListener(nome, function (ev) { var d; try { d = JSON.parse(ev.data); } catch (e) { return; } fn(d); });
533
- };
534
- ouvir("estado", pintarEstado);
535
- ouvir("ocupado", pintarOcupado);
536
- ouvir("historia", reproduzir);
537
- ouvir("limpar", limparConversa);
538
- ouvir("terminal", function (t) { terminalSaida.innerHTML = ""; corTerminal = { cor: null, forte: false }; escreverTerminal(t); });
539
- ouvir("saida", escreverTerminal);
540
- ouvir("voce", function (d) { tratar("voce", d); });
541
- ouvir("agente", function (d) { tratar("agente", d); });
542
- ouvir("comando", function (d) { tratar("comando", d); });
543
- ouvir("erro", function (d) { tratar("erro", d); });
544
- ouvir("arquivos", aoMudarArquivos);
545
- }
546
-
547
- /* ── mandar ──────────────────────────────────────────────────────────── */
548
-
549
- function enviar(texto) {
550
- texto = String(texto || "").trim();
551
- if (!texto) return;
552
- pedir("/api/enviar", { texto: texto });
553
- campo.value = "";
554
- ajustarAltura();
555
- atualizarEnviar();
556
- }
557
- function parar() { pedir("/api/parar", {}); }
558
-
559
- function ajustarAltura() {
560
- campo.style.height = "auto";
561
- campo.style.height = Math.min(campo.scrollHeight, window.innerHeight * 0.4) + "px";
562
- }
563
-
564
- forma.addEventListener("submit", function (ev) {
565
- ev.preventDefault();
566
- if (ocupado && !campo.value.trim()) parar();
567
- else enviar(campo.value);
568
- });
569
- campo.addEventListener("input", function () { ajustarAltura(); atualizarEnviar(); });
570
- campo.addEventListener("keydown", function (ev) {
571
- if (ev.key === "Enter" && !ev.shiftKey && !ev.isComposing) {
572
- ev.preventDefault();
573
- enviar(campo.value);
574
- }
575
- });
576
-
577
- /* Esc Esc para, como no terminal. Um Esc só fecha o que estiver aberto
578
- (menu, diálogo) — parar o agente por um toque errado jogaria fora um
579
- minuto de trabalho. */
580
- var ultimoEsc = 0;
581
- document.addEventListener("keydown", function (ev) {
582
- if (ev.key === "Escape") {
583
- if (!menu.hidden) { fecharMenu(); return; }
584
- var agora = Date.now();
585
- if (ocupado && agora - ultimoEsc < 800) { parar(); ultimoEsc = 0; return; }
586
- ultimoEsc = agora;
587
- return;
588
- }
589
- if ((ev.ctrlKey || ev.metaKey) && (ev.key === "b" || ev.key === "B")) { ev.preventDefault(); alternarLado(); return; }
590
- if ((ev.ctrlKey || ev.metaKey) && (ev.key === "j" || ev.key === "J")) { ev.preventDefault(); alternarTerminal(); return; }
591
- // Tecla de conteúdo devolve o foco ao campo: num chat não se clica para escrever.
592
- var alvo = document.activeElement;
593
- var escrevendo = alvo && (alvo.tagName === "INPUT" || alvo.tagName === "TEXTAREA");
594
- if (ev.key.length === 1 && !ev.metaKey && !ev.ctrlKey && !ev.altKey && !escrevendo && !$("dlgAjuda").open) campo.focus();
595
- });
596
-
597
- /* ── anexar ──────────────────────────────────────────────────────────
598
- O navegador não entrega o CAMINHO de um arquivo, só o conteúdo. Então o
599
- conteúdo sobe para o servidor e entra em state.attachedFiles, igual ao
600
- /attach do terminal. */
601
- var arquivo = $("arquivo");
602
- $("btnAnexar").onclick = function () { arquivo.click(); };
603
- arquivo.onchange = function () { subir(arquivo.files); arquivo.value = ""; };
604
-
605
- function subir(lista) {
606
- Array.prototype.forEach.call(lista || [], function (f) {
607
- if (f.size > 2 * 1024 * 1024) { escreverErro(f.name + " passa de 2 MB. Anexe um arquivo menor ou peça pelo caminho."); return; }
608
- var leitor = new FileReader();
609
- leitor.onload = function () {
610
- var base64 = String(leitor.result).split(",")[1] || "";
611
- pedir("/api/anexar", { nome: f.name, base64: base64 }).then(function (d) {
612
- if (d.estado) pintarEstado(d.estado);
613
- else escreverErro(d.error || "Não consegui anexar " + f.name + ".");
614
- });
615
- };
616
- leitor.readAsDataURL(f);
617
- });
618
- }
619
-
620
- var conta = 0;
621
- window.addEventListener("dragenter", function (ev) { ev.preventDefault(); conta++; document.body.classList.add("soltando"); });
622
- window.addEventListener("dragover", function (ev) { ev.preventDefault(); });
623
- window.addEventListener("dragleave", function () { if (--conta <= 0) { conta = 0; document.body.classList.remove("soltando"); } });
624
- window.addEventListener("drop", function (ev) {
625
- ev.preventDefault();
626
- conta = 0;
627
- document.body.classList.remove("soltando");
628
- var dt = ev.dataTransfer;
629
- if (dt.files && dt.files.length) { subir(dt.files); return; }
630
- // Texto solto (o caminho que o Finder dá com a tecla de atalho) entra no campo.
631
- var texto = "";
632
- try { texto = dt.getData("text/plain") || ""; } catch (e) {}
633
- if (texto) { campo.value = (campo.value ? campo.value.trimEnd() + " " : "") + texto + " "; ajustarAltura(); atualizarEnviar(); campo.focus(); }
634
- });
635
-
636
- /* ── os controles do compositor ─────────────────────────────────────── */
637
-
638
- $("btnPermissao").onclick = function () {
639
- var atual = estado.permissoes || "auto";
640
- abrirMenu(this, [{ titulo: "Quando ele pede licença" }].concat(["auto", "perguntar", "livre"].map(function (m) {
641
- return {
642
- rotulo: PERMISSOES[m].rotulo, desc: PERMISSOES[m].desc, icone: PERMISSOES[m].icone,
643
- marcado: m === atual, classe: m === "livre" ? "laranja" : "",
644
- acao: function () { pedir("/api/permissoes", { modo: m }).then(function (d) { if (d.estado) pintarEstado(d.estado); }); },
645
- };
646
- })));
647
- };
648
-
649
- $("btnEsforco").onclick = function () {
650
- var atual = estado.esforco || "medium";
651
- abrirMenu(this, [{ titulo: "Esforço do agente" }].concat(ESFORCOS.map(function (n) {
652
- return {
653
- rotulo: n.rotulo + " · " + n.nome, desc: n.desc, marcado: n.nome === atual,
654
- acao: function () { pedir("/api/esforco", { nivel: n.nome }).then(function (d) { if (d.estado) pintarEstado(d.estado); }); },
655
- };
656
- })));
657
- };
658
-
659
- $("btnMic").onclick = function () {
660
- // O /voice ouve pelo microfone DO COMPUTADOR (o "ei, primo"), não pela
661
- // página — por isso é um liga/desliga, e o terminal conta o progresso.
662
- pedir("/api/voz", { ligar: !estado.voz }).then(function (d) { if (d.estado) pintarEstado(d.estado); });
663
- };
664
-
665
- /* ── projetos ────────────────────────────────────────────────────────── */
666
-
667
- var projetos = [];
668
- function entrar(nome) {
669
- return pedir("/api/projeto", { nome: nome }).then(function (d) {
670
- if (d.estado) { pintarEstado(d.estado); carregarProjetos(); recarregarPainel(true); }
671
- else escreverErro(d.error || "Não consegui entrar no projeto.");
672
- });
673
- }
674
- function criarProjeto(nome) {
675
- return pedir("/api/projeto", { novo: nome }).then(function (d) {
676
- if (d.estado) { pintarEstado(d.estado); carregarProjetos(); recarregarPainel(true); campo.focus(); }
677
- else escreverErro(d.error || "Não consegui criar a pasta.");
678
- });
679
- }
680
- function menuDeProjetos(dono) {
681
- var itens = [{ titulo: "Projetos em ~/primocode" }];
682
- projetos.slice(0, 12).forEach(function (p) {
683
- itens.push({ rotulo: p.nome, icone: "pasta", marcado: p.nome === estado.projeto, acao: function () { entrar(p.nome); } });
684
- });
685
- if (!projetos.length) itens.push({ titulo: "Nenhum projeto ainda" });
686
- itens.push({ sep: true });
687
- itens.push({ entrada: { id: "nomeNovaPasta", placeholder: "Nova pasta…", botao: "Criar", acao: criarProjeto } });
688
- abrirMenu(dono, itens);
689
- }
690
- $("btnProjeto").onclick = function () { menuDeProjetos(this); };
691
- $("btnNovoProjeto").onclick = function () {
692
- abrirMenu(this, [{ titulo: "Novo projeto em ~/primocode" },
693
- { entrada: { id: "nomeNovoProjeto", placeholder: "Nome da pasta", botao: "Criar", acao: criarProjeto } }]);
694
- };
695
-
696
- function marcarProjetoAtual() {
697
- Array.prototype.forEach.call($("listaProjetos").querySelectorAll(".item"), function (b) {
698
- b.setAttribute("aria-current", b.dataset.nome === estado.projeto ? "true" : "false");
699
- });
700
- }
701
-
702
- function carregarProjetos() {
703
- return pedir("/api/projetos").then(function (d) {
704
- if (!d.ok) return;
705
- projetos = d.projetos || [];
706
- var caixa = $("listaProjetos");
707
- caixa.innerHTML = "";
708
- if (!projetos.length) { caixa.appendChild(el("div", "vazio-lista", "Nenhum projeto")); return; }
709
- projetos.slice(0, 8).forEach(function (p) {
710
- var b = el("button", "item");
711
- b.type = "button";
712
- b.dataset.nome = p.nome;
713
- b.appendChild(icone("pasta"));
714
- b.appendChild(el("span", "titulo", p.nome));
715
- b.onclick = function () { entrar(p.nome); };
716
- caixa.appendChild(b);
717
- });
718
- marcarProjetoAtual();
719
- filtrar();
720
- });
721
- }
722
-
723
- /* ── recentes ────────────────────────────────────────────────────────
724
- Com conta, as conversas da nuvem (as mesmas do /chats e do celular):
725
- clicar continua dali. Sem conta, os últimos pedidos deste computador:
726
- clicar põe o pedido de volta no campo, para repetir ou ajustar. */
727
- function quando(ms) {
728
- if (!ms) return "";
729
- var d = new Date(ms);
730
- if (isNaN(d)) return "";
731
- var dias = Math.floor((Date.now() - d.getTime()) / 86400000);
732
- if (dias <= 0) return d.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
733
- if (dias === 1) return "ontem";
734
- if (dias < 7) return dias + " d";
735
- return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
736
- }
737
-
738
- var conversaAberta = null;
739
- function carregarRecentes() {
740
- return pedir("/api/recentes").then(function (d) {
741
- if (!d.ok) return;
742
- var caixa = $("listaRecentes");
743
- caixa.innerHTML = "";
744
- var itens = d.itens || [];
745
- if (!itens.length) { caixa.appendChild(el("div", "vazio-lista", "Nenhuma conversa ainda")); return; }
746
- itens.slice(0, 20).forEach(function (c) {
747
- var b = el("button", "item");
748
- b.type = "button";
749
- // textContent, nunca innerHTML: o título foi escrito por gente, e um
750
- // <img onerror> ali viraria script na janela que MANDA NO AGENTE.
751
- b.appendChild(el("span", "titulo", c.titulo || "Conversa"));
752
- b.title = c.titulo || "";
753
- if (d.fonte === "nuvem") {
754
- b.setAttribute("aria-current", c.id === conversaAberta ? "true" : "false");
755
- b.onclick = function () { abrirConversa(c); };
756
- } else {
757
- b.onclick = function () { campo.value = c.titulo; ajustarAltura(); atualizarEnviar(); campo.focus(); };
758
- }
759
- caixa.appendChild(b);
760
- });
761
- filtrar();
762
- });
763
- }
764
-
765
- function abrirConversa(c) {
766
- pedir("/api/conversa/abrir", { id: c.id, titulo: c.titulo }).then(function (d) {
767
- if (!d.ok) { escreverErro(d.error || "Não consegui abrir a conversa."); return; }
768
- conversaAberta = c.id;
769
- carregarRecentes();
770
- campo.focus();
771
- });
772
- }
773
-
774
- /* A busca filtra o que já está na barra — recentes e projetos — sem ir ao
775
- servidor: são no máximo vinte linhas. */
776
- var campoBusca = $("campoBusca");
777
- function filtrar() {
778
- var q = (campoBusca.value || "").trim().toLowerCase();
779
- ["listaProjetos", "listaRecentes"].forEach(function (id) {
780
- Array.prototype.forEach.call($(id).querySelectorAll(".item"), function (b) {
781
- b.hidden = !!q && b.textContent.toLowerCase().indexOf(q) < 0;
782
- });
783
- });
784
- }
785
- campoBusca.addEventListener("input", filtrar);
786
- $("btnBusca").onclick = function () {
787
- var caixa = $("busca");
788
- caixa.hidden = !caixa.hidden;
789
- if (!caixa.hidden) campoBusca.focus(); else { campoBusca.value = ""; filtrar(); }
790
- };
791
-
792
- var btnAvisos = $("btnAvisos");
793
- btnAvisos.setAttribute("aria-pressed", avisar ? "true" : "false");
794
- btnAvisos.onclick = function () {
795
- avisar = !avisar;
796
- guardo("primo-avisar", avisar ? "1" : "");
797
- btnAvisos.setAttribute("aria-pressed", avisar ? "true" : "false");
798
- if (avisar && window.Notification && Notification.permission === "default") {
799
- try { Notification.requestPermission(); } catch (e) {}
800
- }
801
- };
802
-
803
- $("btnNovo").onclick = function () {
804
- pedir("/api/novo-chat", {}).then(function (d) {
805
- if (!d.ok) { escreverErro(d.error || "Não deu para começar outro chat agora."); return; }
806
- conversaAberta = null;
807
- limparConversa();
808
- alternarTerminal(false);
809
- carregarRecentes();
810
- campo.focus();
811
- });
812
- };
813
-
814
- /* ── o nome do app, a ajuda e o tema ─────────────────────────────────── */
815
-
816
- var CHAVE_TEMA = "primocode_tema";
817
- function aplicarTema(qual) {
818
- // "sistema" não é um terceiro tema: é seguir o aparelho. Guardar
819
- // "escuro" no lugar faria a janela ignorar o modo claro para sempre.
820
- var real = qual === "sistema"
821
- ? (window.matchMedia("(prefers-color-scheme: light)").matches ? "claro" : "escuro")
822
- : qual;
823
- document.documentElement.setAttribute("data-tema", real);
824
- }
825
- window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", function () {
826
- if (pego(CHAVE_TEMA, "escuro") === "sistema") aplicarTema("sistema");
827
- });
828
-
829
- var dlgAjuda = $("dlgAjuda");
830
- function abrirAjuda() { if (!dlgAjuda.open) dlgAjuda.showModal(); }
831
- $("btnAjuda").onclick = abrirAjuda;
832
- $("navComandos").onclick = abrirAjuda;
833
- $("fecharAjuda").onclick = function () { dlgAjuda.close(); };
834
- $("listaComandos").onclick = function (ev) {
835
- var b = ev.target.closest("[data-cmd]");
836
- if (!b) return;
837
- dlgAjuda.close();
838
- campo.value = b.dataset.cmd;
839
- ajustarAltura(); atualizarEnviar();
840
- campo.focus();
841
- };
842
-
843
- $("btnMarca").onclick = function () {
844
- var tema = pego(CHAVE_TEMA, "escuro");
845
- var escolher = function (t) { return function () { guardo(CHAVE_TEMA, t); aplicarTema(t); }; };
846
- abrirMenu(this, [
847
- { titulo: "Tema" },
848
- { rotulo: "Escuro", marcado: tema === "escuro", acao: escolher("escuro") },
849
- { rotulo: "Claro", marcado: tema === "claro", acao: escolher("claro") },
850
- { rotulo: "Do sistema", marcado: tema === "sistema", acao: escolher("sistema") },
851
- { sep: true },
852
- { rotulo: "Atalhos e comandos", icone: "ajuda", acao: abrirAjuda },
853
- { rotulo: "Terminal", icone: "terminal", acao: function () { alternarTerminal(true); } },
854
- { sep: true },
855
- { titulo: "PrimoCode " + (estado.versao ? "v" + estado.versao : "") },
856
- ]);
857
- };
858
-
859
- /* ── a barra lateral ──────────────────────────────────────────────────
860
- Em tela larga ela fica aberta e a escolha é lembrada; abaixo de 960px
861
- ela flutua por cima da conversa, então começa fechada e fecha sozinha
862
- depois de uma escolha. */
863
- var estreita = window.matchMedia("(max-width: 960px)");
864
- function alternarLado(abrir) {
865
- var aberta = !app.classList.contains("sem-lado");
866
- if (abrir === undefined) abrir = !aberta;
867
- app.classList.toggle("sem-lado", !abrir);
868
- if (!estreita.matches) guardo("primo-lado", abrir ? "aberto" : "fechado");
869
- }
870
- $("btnLado").onclick = function () { alternarLado(false); };
871
- $("btnLadoAbrir").onclick = function () { alternarLado(true); };
872
- function ajustarLado() {
873
- if (estreita.matches) app.classList.add("sem-lado");
874
- else app.classList.toggle("sem-lado", pego("primo-lado", "aberto") === "fechado");
875
- }
876
- estreita.addEventListener("change", ajustarLado);
877
- $("lado").addEventListener("click", function (ev) {
878
- if (estreita.matches && ev.target.closest(".item, #btnNovo, #navTerminal, #navPrevia")) alternarLado(false);
879
- });
880
-
881
- /* ── o painel: Prévia e Arquivos ─────────────────────────────────────
882
- A prévia é o index.html do projeto, servido pela rota com a chave no
883
- caminho (os arquivos relativos da página precisam dela), num iframe
884
- SEM allow-same-origin: o código da prévia não fala com a API. */
885
- var painel = $("painel");
886
- var previa = $("previa");
887
- var aba = "previa";
888
- var entrada = null;
889
- var itensArvore = [];
890
- var abertos = {};
891
- var mudadosAgora = {};
892
- var arquivoAberto = null;
893
-
894
- function abrirPainel(qual) {
895
- painel.hidden = false;
896
- $("btnPainel").setAttribute("aria-pressed", "true");
897
- if (qual) escolherAba(qual);
898
- recarregarPainel(true);
899
- }
900
- function fecharPainel() {
901
- painel.hidden = true;
902
- $("btnPainel").setAttribute("aria-pressed", "false");
903
- previa.removeAttribute("src");
904
- }
905
- $("btnPainel").onclick = function () { if (painel.hidden) abrirPainel(); else fecharPainel(); };
906
- $("fecharPainel").onclick = fecharPainel;
907
- $("navPrevia").onclick = function () { abrirPainel("previa"); };
908
- $("btnRecarregar").onclick = function () { recarregarPainel(true); };
909
-
910
- function escolherAba(qual) {
911
- aba = qual;
912
- $("abaPrevia").setAttribute("aria-selected", qual === "previa" ? "true" : "false");
913
- $("abaArquivos").setAttribute("aria-selected", qual === "arquivos" ? "true" : "false");
914
- $("corpoPrevia").hidden = qual !== "previa";
915
- $("corpoArquivos").hidden = qual !== "arquivos";
916
- }
917
- $("abaPrevia").onclick = function () { escolherAba("previa"); recarregarPainel(true); };
918
- $("abaArquivos").onclick = function () { escolherAba("arquivos"); recarregarPainel(false); };
919
-
920
- function recarregarPainel(previaTambem) {
921
- if (painel.hidden) return;
922
- pedir("/api/arquivos/arvore").then(function (d) {
923
- itensArvore = d.ok ? d.itens || [] : [];
924
- entrada = d.ok ? d.entrada : null;
925
- desenharArvore();
926
- if (previaTambem || aba === "previa") atualizarPrevia();
927
- });
928
- }
929
-
930
- function atualizarPrevia() {
931
- var tem = !!entrada;
932
- $("previaVazia").hidden = tem;
933
- previa.hidden = !tem;
934
- if (!tem) { previa.removeAttribute("src"); return; }
935
- var caminho = entrada.split("/").map(encodeURIComponent).join("/");
936
- // O `?v=` força o navegador a buscar de novo: o mesmo endereço no src
937
- // não recarrega o iframe.
938
- previa.src = "/api/previa/" + CHAVE + "/" + caminho + "?v=" + Date.now();
939
- }
940
-
941
- var espera = null;
942
- function aoMudarArquivos(d) {
943
- (d.mudados || []).forEach(function (m) { mudadosAgora[m] = Date.now(); });
944
- if (painel.hidden) return;
945
- // O agente escreve em rajada (três arquivos num segundo): recarregar a
946
- // cada um faria a prévia piscar. Espera o silêncio.
947
- clearTimeout(espera);
948
- espera = setTimeout(function () {
949
- recarregarPainel(aba === "previa");
950
- if (arquivoAberto && (d.mudados || []).indexOf(arquivoAberto) >= 0) lerArquivo(arquivoAberto);
951
- }, 350);
952
- }
953
-
954
- function desenharArvore() {
955
- var caixa = $("arvore");
956
- caixa.innerHTML = "";
957
- if (!itensArvore.length) {
958
- caixa.appendChild(el("div", "vazio-lista", estado.projeto ? "Pasta vazia" : "Escolha um projeto para ver os arquivos"));
959
- return;
960
- }
961
- itensArvore.forEach(function (it) {
962
- var partes = it.caminho.split("/");
963
- // Some quem está dentro de uma pasta fechada.
964
- for (var i = 1; i < partes.length; i++) {
965
- if (!abertos[partes.slice(0, i).join("/")]) return;
966
- }
967
- var b = el("button", "no");
968
- b.type = "button";
969
- b.setAttribute("role", "treeitem");
970
- b.style.paddingLeft = (8 + (partes.length - 1) * 14) + "px";
971
- if (it.tipo === "pasta") {
972
- b.setAttribute("aria-expanded", abertos[it.caminho] ? "true" : "false");
973
- b.appendChild(icone("pasta"));
974
- b.onclick = function () { abertos[it.caminho] = !abertos[it.caminho]; desenharArvore(); };
975
- } else {
976
- b.appendChild(icone("arquivo"));
977
- b.setAttribute("aria-selected", it.caminho === arquivoAberto ? "true" : "false");
978
- if (mudadosAgora[it.caminho] > Date.now() - 60000) b.classList.add("mudou");
979
- b.onclick = function () { lerArquivo(it.caminho); };
980
- }
981
- b.appendChild(el("span", "nome", partes[partes.length - 1]));
982
- caixa.appendChild(b);
983
- });
984
- }
985
-
986
- function lerArquivo(caminho) {
987
- arquivoAberto = caminho;
988
- desenharArvore();
989
- $("visorNome").textContent = caminho;
990
- var visor = $("visor");
991
- pedir("/api/arquivos/ler?p=" + encodeURIComponent(caminho)).then(function (d) {
992
- visor.innerHTML = "";
993
- if (!d.ok) { visor.textContent = " " + (d.error || "não abriu"); return; }
994
- if (d.binario) { visor.textContent = " Arquivo binário (imagem, áudio…). Não dá para mostrar como texto."; return; }
995
- if (d.grande) { visor.textContent = " Arquivo grande demais para mostrar aqui (mais de 1 MB)."; return; }
996
- var frag = document.createDocumentFragment();
997
- String(d.conteudo || "").split("\n").forEach(function (l) { frag.appendChild(el("span", "ln", l || " ")); });
998
- visor.appendChild(frag);
999
- });
1000
- }
1001
-
1002
- /* ── ligar tudo ──────────────────────────────────────────────────────── */
1003
-
1004
- aplicarTema(pego(CHAVE_TEMA, "escuro"));
1005
- ajustarLado();
1006
- if (window.PrimoMarca) {
1007
- $("mascote").innerHTML = window.PrimoMarca.linha({ tamanho: 56 });
1008
- $("mascotePainel").innerHTML = window.PrimoMarca.linha({ tamanho: 40 });
1009
- }
1010
- pedir("/api/estado").then(function (d) { if (d.__status === 200) pintarEstado(d); }).catch(function () {});
1011
- carregarProjetos();
1012
- carregarRecentes();
1013
- ligar();
1014
- if (ABRIR_PAINEL === "previa" || ABRIR_PAINEL === "arquivos") abrirPainel(ABRIR_PAINEL);
1015
- atualizarEnviar();
1016
- campo.focus();
1017
- })();