primocode 9.8.0-beta.2 → 9.8.0-beta.21

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 (74) hide show
  1. package/README.md +33 -6
  2. package/app/ansi.js +55 -0
  3. package/app/app.css +482 -281
  4. package/app/app.js +888 -315
  5. package/app/index.html +176 -101
  6. package/app/primo-marca.js +58 -1
  7. package/bin/primocode.js +697 -118
  8. package/comandos/explicar.md +14 -0
  9. package/comandos/publicar.md +10 -0
  10. package/comandos/revisar.md +13 -0
  11. package/lib/act.js +130 -26
  12. package/lib/api.js +8 -1
  13. package/lib/app.js +317 -74
  14. package/lib/browser.js +79 -3
  15. package/lib/catalogo.js +11 -3
  16. package/lib/checkpoint-git.js +218 -0
  17. package/lib/checkpoint.js +249 -0
  18. package/lib/claude-engine.js +25 -3
  19. package/lib/comandos-extra.js +481 -0
  20. package/lib/comandos.js +104 -0
  21. package/lib/config.js +21 -4
  22. package/lib/estudio/acervo-site.js +157 -0
  23. package/lib/estudio/assets.js +77 -3
  24. package/lib/estudio/cinema.js +354 -0
  25. package/lib/estudio/composicao.js +470 -0
  26. package/lib/estudio/edicao.js +450 -0
  27. package/lib/estudio/impacto.js +263 -0
  28. package/lib/estudio/index.js +769 -5
  29. package/lib/estudio/plano.js +5 -2
  30. package/lib/estudio/remotion.js +228 -23
  31. package/lib/estudio/slides.js +34 -30
  32. package/lib/fala.js +95 -1
  33. package/lib/local/cerebro.json +361 -180
  34. package/lib/local/entrada.js +18 -8
  35. package/lib/local/motor-cli.js +1 -1
  36. package/lib/mencoes.js +98 -0
  37. package/lib/offline.js +168 -0
  38. package/lib/painel.js +291 -0
  39. package/lib/perigo.js +256 -0
  40. package/lib/pre-leitura.js +118 -0
  41. package/lib/repeticao.js +4 -2
  42. package/lib/skills.js +6 -1
  43. package/lib/tempos.js +62 -0
  44. package/lib/ui.js +171 -17
  45. package/package.json +3 -2
  46. package/skills/apresentacao-pptx/SKILL.md +52 -0
  47. package/skills/cartaz-de-arte/LICENSE.txt +202 -0
  48. package/skills/cartaz-de-arte/SKILL.md +47 -0
  49. package/skills/criar-skill/LICENSE.txt +202 -0
  50. package/skills/criar-skill/SKILL.md +49 -0
  51. package/skills/depurar-de-verdade/LICENSE.txt +21 -0
  52. package/skills/depurar-de-verdade/SKILL.md +14 -5
  53. package/skills/design-de-verdade/LICENSE.txt +177 -0
  54. package/skills/design-de-verdade/SKILL.md +42 -0
  55. package/skills/documento-word/SKILL.md +57 -0
  56. package/skills/interface-revisada/SKILL.md +47 -0
  57. package/skills/me-ensina/LICENSE.txt +21 -0
  58. package/skills/me-ensina/SKILL.md +27 -0
  59. package/skills/me-entrevista/LICENSE.txt +21 -0
  60. package/skills/me-entrevista/SKILL.md +27 -0
  61. package/skills/motion-de-primeira/SKILL.md +1 -1
  62. package/skills/pdf-bonito/SKILL.md +48 -0
  63. package/skills/planilha-excel/SKILL.md +47 -0
  64. package/skills/react-sem-dor/SKILL.md +48 -0
  65. package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
  66. package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
  67. package/skills/site-com-motion/SKILL.md +39 -0
  68. package/skills/temas-prontos/LICENSE.txt +202 -0
  69. package/skills/temas-prontos/SKILL.md +50 -0
  70. package/skills/testar-o-site/LICENSE.txt +202 -0
  71. package/skills/testar-o-site/SKILL.md +46 -0
  72. package/skills/video-no-remotion/SKILL.md +48 -0
  73. package/studio/narrar.py +111 -10
  74. package/studio/rostos.py +52 -0
package/app/app.js CHANGED
@@ -1,444 +1,1017 @@
1
- /* A janela conversando com o PrimoCode.
1
+ /* A janela do PrimoCode conversando com o agente.
2
2
  *
3
- * Nada de framework: é a mesma regra do resto do projeto, e aqui ela nem
4
- * custa — a página tem um campo, uma lista e um fluxo de eventos.
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).
5
6
  *
6
- * O trabalho de verdade é traduzir a saída do terminal. O agente escreve com
7
- * códigos ANSI, e jogar isso na tela como texto puro mostraria "[38;5;45m" no
8
- * meio das frases. Então os códigos viram <span> com a cor certa — as mesmas
9
- * cores do lib/ui.js, para as duas telas serem o mesmo produto.
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".
10
11
  */
11
12
  (function () {
12
13
  "use strict";
13
14
 
14
- /* A chave chega na URL, na primeira abertura. Ela sai de lá assim que é
15
- lida — numa aba comum ficaria à mostra e no histórico do navegador — mas
16
- antes vai para o sessionStorage: sem isso, apertar F5 matava a janela,
17
- porque a URL limpa não tem mais chave nenhuma. */
18
- var CHAVE = new URLSearchParams(location.search).get("k") || "";
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") || "";
19
21
  try {
20
22
  if (CHAVE) sessionStorage.setItem("primo-chave", CHAVE);
21
23
  else CHAVE = sessionStorage.getItem("primo-chave") || "";
22
24
  } catch (e) { /* sem sessionStorage: vale só esta abertura */ }
23
25
  if (location.search) history.replaceState(null, "", location.pathname);
24
26
 
25
- var conversa = document.getElementById("conversa");
26
- var rolo = document.getElementById("rolo");
27
- var campo = document.getElementById("campo");
28
- var forma = document.getElementById("forma");
29
- var mandar = document.getElementById("mandar");
30
- var parar = document.getElementById("parar");
31
- var pensando = document.getElementById("pensando");
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 = {};
32
36
  var ocupado = false;
33
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
+
34
41
  function pedir(rota, corpo) {
35
42
  return fetch(rota, {
36
43
  method: corpo === undefined ? "GET" : "POST",
37
44
  headers: { "Content-Type": "application/json", "X-Primo-Chave": CHAVE },
38
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; });
39
49
  });
40
50
  }
41
51
 
42
- /* ── rolagem ───────────────────────────────────────────────────────────
43
- Só acompanha se a pessoa JÁ estava embaixo. Puxar a tela de volta para o
44
- fim enquanto alguém lê o que passou é a coisa mais irritante que um log
45
- ao vivo faz. */
46
- function noFim() {
47
- return rolo.scrollTop + rolo.clientHeight >= rolo.scrollHeight - 60;
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;
48
57
  }
49
- function acompanhar(estava) {
50
- if (estava) rolo.scrollTop = rolo.scrollHeight;
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;
51
67
  }
52
-
53
- function limparBoasVindas() {
54
- var b = document.getElementById("vazio");
55
- if (b) b.hidden = true;
56
- // A tira das mensagens nasce escondida para a tela vazia poder ocupar o
57
- // meio. Esconder uma sem mostrar a outra deixaria a janela em branco na
58
- // primeira mensagem — e nada na tela é o pior jeito de responder.
59
- var t = document.getElementById("conversa");
60
- if (t) t.hidden = false;
68
+ function fugir(s) {
69
+ return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
61
70
  }
62
71
 
63
- function voltarAoVazio() {
64
- var b = document.getElementById("vazio");
65
- var t = document.getElementById("conversa");
66
- if (t) { t.innerHTML = ""; t.hidden = true; }
67
- if (b) b.hidden = false;
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];
68
94
  }
69
95
 
70
- /* ── ANSI → HTML ───────────────────────────────────────────────────────
71
- Só o que o PrimoCode realmente emite: SGR de cor 256 (38;5;N), negrito e
72
- reset. O resto dos códigos (mover cursor, apagar linha) é desenho de
73
- terminal e é descartado — na página quem rola é o navegador. */
74
- var ANSI = /\x1b\[([0-9;?]*)([a-zA-Z])/g;
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
+ }
75
109
 
76
- function fugir(s) {
77
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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();
78
155
  }
79
156
 
80
- function paraHtml(texto, estado) {
81
- var fora = "";
82
- var ultimo = 0;
83
- var m;
84
- ANSI.lastIndex = 0;
157
+ document.addEventListener("mousedown", function (ev) {
158
+ if (!menu.hidden && !menu.contains(ev.target) && menuDono && !menuDono.contains(ev.target)) fecharMenu();
159
+ });
85
160
 
86
- function abrir() {
87
- var classes = [];
88
- if (estado.cor) classes.push("c" + estado.cor);
89
- if (estado.forte) classes.push("forte");
90
- return classes.length ? '<span class="' + classes.join(" ") + '">' : "";
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>");
91
197
  }
92
- function fechar() {
93
- return (estado.cor || estado.forte) ? "</span>" : "";
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);
94
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" };
95
269
 
96
- while ((m = ANSI.exec(texto)) !== null) {
97
- var pedaco = texto.slice(ultimo, m.index);
98
- if (pedaco) fora += abrir() + fugir(pedaco) + fechar();
99
- ultimo = m.index + m[0].length;
100
- if (m[2] !== "m") continue; // não é cor: descarta
101
- var partes = m[1].split(";");
102
- for (var i = 0; i < partes.length; i++) {
103
- var n = partes[i];
104
- if (n === "" || n === "0") { estado.cor = null; estado.forte = false; }
105
- else if (n === "1") estado.forte = true;
106
- else if (n === "38" && partes[i + 1] === "5") { estado.cor = partes[i + 2]; i += 2; }
107
- else if (n === "39") estado.cor = null;
108
- else if (n === "22") estado.forte = false;
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));
109
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
+ };
110
332
  }
111
- var resto = texto.slice(ultimo);
112
- if (resto) fora += abrir() + fugir(resto) + fechar();
113
- return fora;
114
333
  }
115
334
 
116
- /* O estado de cor atravessa os pedaços: o Node corta a saída onde der, e um
117
- "\x1b[38;5;45m" pode chegar num pedaço e o texto colorido no seguinte.
118
- Guardando o estado fora da função, a cor continua do outro lado do corte. */
119
- var corrente = { cor: null, forte: false };
120
- var bloco = null;
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
+ }
121
343
 
122
- function escreverSaida(texto) {
344
+ function aoAgente(e) {
123
345
  var estava = noFim();
124
- limparBoasVindas();
125
- if (!bloco) {
126
- bloco = document.createElement("pre");
127
- bloco.className = "saida";
128
- conversa.appendChild(bloco);
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;
129
375
  }
130
- bloco.insertAdjacentHTML("beforeend", paraHtml(texto, corrente));
131
376
  acompanhar(estava);
132
377
  }
133
378
 
134
- function escreverVoce(texto) {
135
- var estava = noFim();
136
- limparBoasVindas();
137
- var linha = document.createElement("div");
138
- linha.className = "voce";
139
- var caixa = document.createElement("div");
140
- caixa.textContent = texto;
141
- linha.appendChild(caixa);
142
- conversa.appendChild(linha);
143
- bloco = null; // a próxima saída começa bloco novo
144
- corrente = { cor: null, forte: false };
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);
145
384
  acompanhar(true);
146
- void estava;
147
385
  }
148
386
 
149
387
  function escreverErro(texto) {
150
- var estava = noFim();
151
- var d = document.createElement("div");
152
- d.className = "aviso-erro";
153
- d.textContent = texto;
154
- conversa.appendChild(d);
155
- bloco = null;
156
- acompanhar(estava);
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;
157
403
  }
158
404
 
159
- /* ── estado ────────────────────────────────────────────────────────────── */
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
+ }
160
443
 
161
444
  function pintarEstado(e) {
162
445
  if (!e) return;
163
- if (e.versao) document.getElementById("versao").textContent = "v" + e.versao;
164
- document.getElementById("pasta").textContent = e.pasta || "";
165
- document.getElementById("modo").textContent = e.modo || "";
166
- document.getElementById("esforco").textContent = e.esforco || "";
167
- document.title = (e.projeto ? e.projeto + " — " : "") + "PrimoCode";
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
+ }
168
510
  }
169
511
 
512
+ var avisar = pego("primo-avisar", "") === "1";
170
513
  function pintarOcupado(v) {
514
+ var era = ocupado;
171
515
  ocupado = !!v;
172
- pensando.hidden = !ocupado;
173
- parar.hidden = !ocupado;
174
- mandar.hidden = ocupado;
175
- campo.placeholder = ocupado
176
- ? "ele está trabalhando — pode ir escrevendo o próximo"
177
- // O "Enter manda, Shift+Enter quebra linha" saiu daqui: ele já está
178
- // escrito na dica logo abaixo do campo, e dizer a mesma coisa duas
179
- // vezes na mesma tela faz a pessoa ler nenhuma das duas.
180
- : "Peça alguma coisa…";
181
- }
182
-
183
- /* ── o fluxo ───────────────────────────────────────────────────────────
184
- EventSource reconecta sozinho quando a conexão cai, e o servidor manda o
185
- que já saiu ao ligar — então uma queda de rede local não deixa buraco na
186
- conversa. */
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. */
187
529
  function ligar() {
188
530
  var fonte = new EventSource("/api/fluxo?k=" + encodeURIComponent(CHAVE));
189
- fonte.addEventListener("saida", function (ev) { escreverSaida(JSON.parse(ev.data)); });
190
- fonte.addEventListener("voce", function (ev) { escreverVoce(JSON.parse(ev.data)); });
191
- fonte.addEventListener("erro", function (ev) { escreverErro(JSON.parse(ev.data)); });
192
- fonte.addEventListener("estado", function (ev) { pintarEstado(JSON.parse(ev.data)); });
193
- fonte.addEventListener("ocupado", function (ev) { pintarOcupado(JSON.parse(ev.data)); });
194
- fonte.onerror = function () { /* o EventSource se vira sozinho */ };
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);
195
545
  }
196
546
 
197
- /* ── mandar ────────────────────────────────────────────────────────────── */
547
+ /* ── mandar ──────────────────────────────────────────────────────────── */
198
548
 
199
549
  function enviar(texto) {
200
- texto = (texto || "").trim();
550
+ texto = String(texto || "").trim();
201
551
  if (!texto) return;
202
552
  pedir("/api/enviar", { texto: texto });
203
553
  campo.value = "";
204
554
  ajustarAltura();
555
+ atualizarEnviar();
205
556
  }
557
+ function parar() { pedir("/api/parar", {}); }
206
558
 
207
559
  function ajustarAltura() {
208
560
  campo.style.height = "auto";
209
- campo.style.height = Math.min(campo.scrollHeight, window.innerHeight * 0.44) + "px";
561
+ campo.style.height = Math.min(campo.scrollHeight, window.innerHeight * 0.4) + "px";
210
562
  }
211
563
 
212
564
  forma.addEventListener("submit", function (ev) {
213
565
  ev.preventDefault();
214
- enviar(campo.value);
566
+ if (ocupado && !campo.value.trim()) parar();
567
+ else enviar(campo.value);
215
568
  });
216
-
217
- campo.addEventListener("input", ajustarAltura);
569
+ campo.addEventListener("input", function () { ajustarAltura(); atualizarEnviar(); });
218
570
  campo.addEventListener("keydown", function (ev) {
219
- if (ev.key === "Enter" && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
571
+ if (ev.key === "Enter" && !ev.shiftKey && !ev.isComposing) {
220
572
  ev.preventDefault();
221
573
  enviar(campo.value);
222
574
  }
223
575
  });
224
576
 
225
- parar.addEventListener("click", function () { pedir("/api/parar", {}); });
226
-
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;
227
581
  document.addEventListener("keydown", function (ev) {
228
- if (ev.key === "Escape" && ocupado) { pedir("/api/parar", {}); return; }
229
- // Qualquer tecla de conteúdo devolve o foco ao campo: numa janela de chat
230
- // não se clica para escrever.
231
- if (ev.key.length === 1 && !ev.metaKey && !ev.ctrlKey && document.activeElement !== campo) {
232
- campo.focus();
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;
233
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();
234
595
  });
235
596
 
236
- document.querySelectorAll(".sugestao").forEach(function (b) {
237
- b.addEventListener("click", function () { enviar(b.dataset.manda); });
238
- });
239
-
240
- /* ── arrastar arquivo ──────────────────────────────────────────────────
241
- "Eu quero que eu consiga arrastar os meus vídeos pra dentro do terminal,
242
- e também lá no desktop."
243
-
244
- O navegador não entrega o CAMINHO de um arquivo solto, por segurança — só
245
- o conteúdo. Mas o PrimoCode roda na mesma máquina: o que ele precisa é do
246
- caminho, e o nome sozinho não basta.
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
+ }
247
619
 
248
- Duas portas, então. Soltar o arquivo escreve o nome no campo com uma
249
- frase que o agente entende ("o arquivo X, que está em..."), e soltar TEXTO
250
- — que é o que o Finder e o Explorer dão ao arrastar com a tecla de atalho,
251
- e o que todo terminal recebe — entra como caminho de verdade. */
252
620
  var conta = 0;
253
- window.addEventListener("dragenter", function (ev) {
254
- ev.preventDefault(); conta++; document.body.classList.add("soltando");
255
- });
621
+ window.addEventListener("dragenter", function (ev) { ev.preventDefault(); conta++; document.body.classList.add("soltando"); });
256
622
  window.addEventListener("dragover", function (ev) { ev.preventDefault(); });
257
- window.addEventListener("dragleave", function () {
258
- if (--conta <= 0) { conta = 0; document.body.classList.remove("soltando"); }
259
- });
623
+ window.addEventListener("dragleave", function () { if (--conta <= 0) { conta = 0; document.body.classList.remove("soltando"); } });
260
624
  window.addEventListener("drop", function (ev) {
261
625
  ev.preventDefault();
262
626
  conta = 0;
263
627
  document.body.classList.remove("soltando");
264
-
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.
265
631
  var texto = "";
266
- try { texto = ev.dataTransfer.getData("text/plain") || ""; } catch (e) {}
267
- if (!texto && ev.dataTransfer.files && ev.dataTransfer.files.length) {
268
- texto = Array.prototype.map.call(ev.dataTransfer.files, function (f) {
269
- // Sem caminho: o nome, e o aviso de onde procurar. É o que dá para
270
- // dizer com honestidade daqui.
271
- return f.name;
272
- }).join(" ");
273
- if (texto) {
274
- texto = "o arquivo " + texto + " (procure na pasta de Downloads ou na "
275
- + "Área de Trabalho; se não achar, me peça o caminho completo)";
276
- }
277
- }
278
- if (!texto) return;
279
- campo.value = (campo.value ? campo.value.trimEnd() + " " : "") + texto + " ";
280
- campo.focus();
281
- ajustarAltura();
632
+ try { texto = dt.getData("text/plain") || ""; } catch (e) {}
633
+ if (texto) { campo.value = (campo.value ? campo.value.trimEnd() + " " : "") + texto + " "; ajustarAltura(); atualizarEnviar(); campo.focus(); }
282
634
  });
283
635
 
284
- /* ── a barra lateral ─────────────────────────────────────────────────
285
- Mascote, novo chat, histórico e configurações — a mesma coisa da página
286
- /code do Conecta, e de propósito: quem sai de uma tela para a outra não
287
- pode ter de reaprender onde ficam as coisas. */
288
-
289
- var CHAVE_TEMA = "primocode_tema";
290
- var CHAVE_COR = "primocode_cor";
291
- var pego = function (k, p) { try { return localStorage.getItem(k) || p; } catch (e) { return p; } };
292
- var guardo = function (k, v) { try { localStorage.setItem(k, v); } catch (e) {} };
293
-
294
- function marcarEscolha(caixaId, atributo, valor) {
295
- var caixa = document.getElementById(caixaId);
296
- if (!caixa) return;
297
- Array.prototype.forEach.call(caixa.children, function (b) {
298
- b.setAttribute("aria-pressed", b.dataset[atributo] === valor ? "true" : "false");
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.");
299
672
  });
300
673
  }
301
- function aplicarTema(qual) {
302
- // "sistema" não é um terceiro tema: é a ausência de escolha. Guardar
303
- // "escuro" quando a pessoa quer seguir o aparelho faria a janela ignorar
304
- // o modo noturno para sempre, sem ela ter como saber por quê.
305
- var raiz = document.documentElement;
306
- if (qual === "sistema") {
307
- raiz.setAttribute("data-tema",
308
- window.matchMedia("(prefers-color-scheme: dark)").matches ? "escuro" : "claro");
309
- } else {
310
- raiz.setAttribute("data-tema", qual);
311
- }
312
- marcarEscolha("temas", "tema", qual);
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
+ });
313
679
  }
314
- function aplicarCor(cor) {
315
- document.documentElement.setAttribute("data-cor", cor);
316
- marcarEscolha("cores", "cor", cor);
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
+ });
317
700
  }
318
701
 
319
- /* A LOGO — a mesma do ícone do sistema e da barra lateral do Conecta. Onde
320
- o produto se apresenta vai ela; o mascote em pixel fica para onde ele é
321
- personagem. */
322
- function desenharMarca() {
323
- if (!window.PrimoMarca) return;
324
- var t = document.getElementById("marcaTopo");
325
- var g = document.getElementById("marcaGrande");
326
- var desenha = window.PrimoMarca.logo || window.PrimoMarca.svg;
327
- if (t) t.innerHTML = desenha({ tamanho: 22 });
328
- if (g) g.innerHTML = desenha({ tamanho: 82, rotulo: "PrimoCode" });
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
+ });
329
721
  }
330
722
 
331
- function quando(iso) {
332
- if (!iso) return "";
333
- var d = new Date(iso);
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);
334
730
  if (isNaN(d)) return "";
335
731
  var dias = Math.floor((Date.now() - d.getTime()) / 86400000);
336
732
  if (dias <= 0) return d.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
337
733
  if (dias === 1) return "ontem";
338
- if (dias < 7) return dias + " dias";
734
+ if (dias < 7) return dias + " d";
339
735
  return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
340
736
  }
341
737
 
342
738
  var conversaAberta = null;
343
-
344
- function carregarConversas() {
345
- var caixa = document.getElementById("recentes");
346
- if (!caixa) return;
347
- pedir("/api/conversas").then(function (r) { return r.json(); }).then(function (d) {
348
- if (!d || !d.ok || !d.conversas || !d.conversas.length) return; // deixa o texto de vazio
739
+ function carregarRecentes() {
740
+ return pedir("/api/recentes").then(function (d) {
741
+ if (!d.ok) return;
742
+ var caixa = $("listaRecentes");
349
743
  caixa.innerHTML = "";
350
- d.conversas.forEach(function (c) {
351
- var b = document.createElement("button");
352
- b.className = "item";
353
- b.setAttribute("aria-current", c.id === conversaAberta ? "true" : "false");
354
- var t = document.createElement("span");
355
- // textContent, nunca innerHTML: o título vem cifrado do banco e foi
356
- // escrito por gente — um "<img onerror>" ali viraria script na janela.
357
- t.textContent = c.titulo || "Conversa";
358
- var q = document.createElement("span");
359
- q.className = "quando";
360
- q.textContent = quando(c.atualizada) + (c.mensagens ? " · " + c.mensagens + " msg" : "");
361
- b.appendChild(t); b.appendChild(q);
362
- b.onclick = function () { abrirConversa(c.id); };
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
+ }
363
759
  caixa.appendChild(b);
364
760
  });
365
- }).catch(function () { /* sem conta: fica o texto de vazio */ });
366
- }
367
-
368
- function abrirConversa(id) {
369
- conversaAberta = id;
370
- pedir("/api/conversa/" + encodeURIComponent(id)).then(function (r) { return r.json(); })
371
- .then(function (d) {
372
- if (!d || !d.ok) return;
373
- voltarAoVazio();
374
- limparBoasVindas();
375
- var tira = document.getElementById("conversa");
376
- (d.mensagens || []).forEach(function (m) {
377
- var linha = document.createElement("div");
378
- if (m.papel === "user") {
379
- linha.className = "voce";
380
- var dentro = document.createElement("div");
381
- dentro.textContent = m.texto;
382
- linha.appendChild(dentro);
383
- } else {
384
- linha.className = "saida";
385
- linha.textContent = m.texto;
386
- }
387
- tira.appendChild(linha);
388
- });
389
- rolo.scrollTop = rolo.scrollHeight;
390
- carregarConversas();
391
- }).catch(function () {});
761
+ filtrar();
762
+ });
392
763
  }
393
764
 
394
- function ligarLado() {
395
- var novo = document.getElementById("btnNovo");
396
- if (novo) novo.onclick = function () {
397
- conversaAberta = null;
398
- voltarAoVazio();
399
- carregarConversas();
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();
400
770
  campo.focus();
401
- };
771
+ });
772
+ }
402
773
 
403
- var dlg = document.getElementById("dlgConfig");
404
- var abrir = document.getElementById("btnConfig");
405
- var fechar = document.getElementById("fecharConfig");
406
- if (abrir && dlg) abrir.onclick = function () {
407
- // Os três valores vêm do estado que o terminal já manda; repetir a
408
- // consulta aqui só para abrir um diálogo seria uma ida à toa.
409
- ["pasta", "modo", "esforco"].forEach(function (k) {
410
- var de = document.getElementById(k);
411
- var para = document.getElementById(k + "Config");
412
- if (de && para) para.textContent = de.textContent || "—";
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;
413
782
  });
414
- dlg.showModal();
415
- };
416
- if (fechar && dlg) fechar.onclick = function () { dlg.close(); };
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
+ };
417
802
 
418
- var temas = document.getElementById("temas");
419
- if (temas) temas.onclick = function (ev) {
420
- var b = ev.target.closest("[data-tema]"); if (!b) return;
421
- guardo(CHAVE_TEMA, b.dataset.tema); aplicarTema(b.dataset.tema);
422
- };
423
- var cores = document.getElementById("cores");
424
- if (cores) cores.onclick = function (ev) {
425
- var b = ev.target.closest("[data-cor]"); if (!b) return;
426
- guardo(CHAVE_COR, b.dataset.cor); aplicarCor(b.dataset.cor);
427
- };
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
+ }
428
953
 
429
- window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function () {
430
- if (pego(CHAVE_TEMA, "escuro") === "sistema") aplicarTema("sistema");
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);
431
983
  });
432
984
  }
433
985
 
434
- desenharMarca();
435
- aplicarTema(pego(CHAVE_TEMA, "escuro"));
436
- aplicarCor(pego(CHAVE_COR, "ciano"));
437
- ligarLado();
438
- carregarConversas();
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 ──────────────────────────────────────────────────────── */
439
1003
 
440
- pedir("/api/estado").then(function (r) { return r.json(); }).then(pintarEstado)
441
- .catch(function () {});
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();
442
1013
  ligar();
1014
+ if (ABRIR_PAINEL === "previa" || ABRIR_PAINEL === "arquivos") abrirPainel(ABRIR_PAINEL);
1015
+ atualizarEnviar();
443
1016
  campo.focus();
444
1017
  })();