primocode 8.22.1 → 8.26.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 (41) hide show
  1. package/app/app.css +230 -0
  2. package/app/app.js +273 -0
  3. package/app/icone.svg +6 -0
  4. package/app/index.html +68 -0
  5. package/bin/primocode.js +52 -0
  6. package/lib/app.js +334 -0
  7. package/lib/catalogo.js +2 -0
  8. package/lib/config.js +53 -2
  9. package/lib/studio.js +125 -11
  10. package/lib/tools.js +35 -1
  11. package/package.json +3 -2
  12. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  13. package/studio/arquivos/corte-estatico-corte-a5bab.json +150 -0
  14. package/studio/arquivos/corte-founders-ai-teste-do-player-2de1d.json +65 -0
  15. package/studio/arquivos/corte-legado-teste.json +1 -0
  16. package/studio/arquivos/corte-legado2.json +1 -0
  17. package/studio/arquivos/corte-minimo-2a829.json +24 -0
  18. package/studio/arquivos/corte-ok-00a3b.json +18 -0
  19. package/studio/arquivos/corte-ok-1270e.json +22 -0
  20. package/studio/arquivos/corte-sweep-corte-cfb29.json +147 -0
  21. package/studio/arquivos/corte-visual-completo-78f50.json +35 -0
  22. package/studio/arquivos/grade-sweep-grade-0c2f1.json +119 -0
  23. package/studio/arquivos/prisma-estatico-prisma-d8740.json +182 -0
  24. package/studio/arquivos/prisma-sweep-prisma-4c58c.json +166 -0
  25. package/studio/arquivos/prisma-y-b0785.json +52 -0
  26. package/studio/arquivos/prosa-sweep-prosa-f29cf.json +80 -0
  27. package/studio/arquivos/tela-estatico-tela-c7daf.json +90 -0
  28. package/studio/arquivos/tela-sweep-tela-0d2bf.json +84 -0
  29. package/studio/arquivos/traco-estatico-traco-25872.json +113 -0
  30. package/studio/arquivos/traco-sweep-traco-0bcf4.json +102 -0
  31. package/studio/ferramentas/corte.md +159 -12
  32. package/studio/server.py +264 -8
  33. package/studio/web/analise.html +52 -0
  34. package/studio/web/js/analise.js +202 -0
  35. package/studio/web/js/exportar.js +88 -1
  36. package/studio/web/js/player.js +172 -46
  37. package/studio/web/js/render.js +99 -1
  38. package/studio/web/js/video.js +434 -0
  39. package/studio/web/render.css +27 -0
  40. package/studio/web/tool.html +1 -0
  41. package/studio/web/view.html +2 -0
package/app/app.css ADDED
@@ -0,0 +1,230 @@
1
+ /* A janela do PrimoCode.
2
+ *
3
+ * "Um aplicativo desktop bem bonito com as cores do PrimoCode, com aquele
4
+ * azul. Bem estilo Codex. Um chat estilo terminal para quem não quer o
5
+ * terminal."
6
+ *
7
+ * As cores não foram escolhidas aqui: são as mesmas de lib/ui.js, que já vêm
8
+ * do design system do produto. cyan-400 é o azul do PrimoCode; slate é a
9
+ * neutra; amber avisa e rose reprova. Repetir o terminal na tela não é falta
10
+ * de imaginação — é o que faz as duas telas serem o MESMO produto.
11
+ */
12
+
13
+ :root {
14
+ --azul: #22d3ee; /* cyan-400 — a marca */
15
+ --azul-fundo: #0891b2; /* cyan-600 — a mesma cor recuando */
16
+ --texto: #e2e8f0; /* slate-200 */
17
+ --fraco: #94a3b8; /* slate-400 */
18
+ --apagado: #64748b; /* slate-500 */
19
+ --alerta: #fbbf24; /* amber-400 */
20
+ --falha: #fb7185; /* rose-400 */
21
+
22
+ --fundo: #0a0f16;
23
+ --painel: #0f1620;
24
+ --borda: #1e293b;
25
+ --borda-viva: #24384f;
26
+
27
+ --mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", Menlo,
28
+ Consolas, "Liberation Mono", monospace;
29
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
30
+ }
31
+
32
+ * { box-sizing: border-box; }
33
+
34
+ /* `display:flex` e `display:grid` nas regras abaixo ganham do `[hidden]` do
35
+ navegador, que é só `display:none` sem peso nenhum. O resultado era o botão
36
+ de parar e o "trabalhando" aparecendo com o agente parado — a janela dizia
37
+ que estava fazendo alguma coisa o tempo todo. */
38
+ [hidden] { display: none !important; }
39
+
40
+ html, body {
41
+ height: 100%;
42
+ margin: 0;
43
+ background: var(--fundo);
44
+ color: var(--texto);
45
+ font-family: var(--sans);
46
+ font-size: 14px;
47
+ overflow: hidden;
48
+ }
49
+
50
+ body {
51
+ display: grid;
52
+ grid-template-rows: auto 1fr auto;
53
+ /* O brilho azul atrás de tudo. Um só, bem aberto e bem fraco: dá
54
+ profundidade sem virar papel de parede. */
55
+ background-image:
56
+ radial-gradient(120% 80% at 50% -20%, rgba(34, 211, 238, .08), transparent 62%);
57
+ }
58
+
59
+ /* ── topo ───────────────────────────────────────────────────────────────── */
60
+
61
+ #topo {
62
+ display: flex; align-items: center; justify-content: space-between;
63
+ gap: 16px; padding: 11px 18px;
64
+ border-bottom: 1px solid var(--borda);
65
+ background: rgba(10, 15, 22, .72);
66
+ backdrop-filter: blur(14px);
67
+ /* Na janela do Chrome em modo app não há barra de título: esta faixa é a
68
+ barra, e precisa poder arrastar a janela. */
69
+ -webkit-app-region: drag;
70
+ user-select: none;
71
+ }
72
+ #topo button, #topo .chip { -webkit-app-region: no-drag; }
73
+
74
+ .marca { display: flex; align-items: center; gap: 9px; }
75
+ .logo { width: 19px; height: 19px; color: var(--azul); flex: 0 0 auto; }
76
+ .nome { font-weight: 640; letter-spacing: -.012em; }
77
+ .versao { color: var(--apagado); font-size: 11.5px; font-family: var(--mono); }
78
+
79
+ .estado { display: flex; gap: 7px; align-items: center; min-width: 0; }
80
+ .chip {
81
+ font-family: var(--mono); font-size: 11.5px;
82
+ color: var(--fraco); background: var(--painel);
83
+ border: 1px solid var(--borda); border-radius: 999px;
84
+ padding: 3px 10px; white-space: nowrap;
85
+ max-width: 300px; overflow: hidden; text-overflow: ellipsis;
86
+ }
87
+ .chip:empty { display: none; }
88
+ #modo { color: var(--azul); border-color: var(--borda-viva); }
89
+
90
+ /* ── conversa ───────────────────────────────────────────────────────────── */
91
+
92
+ #rolo { overflow-y: auto; scroll-behavior: smooth; }
93
+ #rolo::-webkit-scrollbar { width: 10px; }
94
+ #rolo::-webkit-scrollbar-thumb { background: #1c2836; border-radius: 8px; border: 3px solid var(--fundo); }
95
+
96
+ #conversa { max-width: 860px; margin: 0 auto; padding: 26px 22px 40px; }
97
+
98
+ .boas-vindas { padding: 12vh 0 0; text-align: center; }
99
+ .boas-vindas h1 {
100
+ font-size: 27px; font-weight: 620; letter-spacing: -.025em; margin: 0 0 10px;
101
+ }
102
+ .boas-vindas p {
103
+ color: var(--fraco); max-width: 460px; margin: 0 auto 26px; line-height: 1.6;
104
+ }
105
+ .atalhos { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
106
+ .atalhos button {
107
+ font: inherit; font-size: 13px; color: var(--fraco); cursor: pointer;
108
+ background: var(--painel); border: 1px solid var(--borda);
109
+ border-radius: 999px; padding: 7px 15px;
110
+ transition: border-color .16s, color .16s, transform .16s;
111
+ }
112
+ .atalhos button:hover {
113
+ color: var(--texto); border-color: var(--borda-viva); transform: translateY(-1px);
114
+ }
115
+
116
+ /* O que o usuário disse: bolha à direita, curta, com a marca de lado. */
117
+ .voce {
118
+ margin: 24px 0 14px; display: flex; justify-content: flex-end;
119
+ }
120
+ .voce > div {
121
+ background: linear-gradient(180deg, rgba(34,211,238,.12), rgba(34,211,238,.07));
122
+ border: 1px solid rgba(34, 211, 238, .28);
123
+ border-radius: 14px 14px 4px 14px;
124
+ padding: 10px 15px; max-width: 78%;
125
+ white-space: pre-wrap; word-break: break-word; line-height: 1.55;
126
+ }
127
+
128
+ /* A saída do agente: monoespaçada, como no terminal, porque É a saída do
129
+ terminal — as mesmas linhas, as mesmas cores. */
130
+ .saida {
131
+ font-family: var(--mono); font-size: 12.8px; line-height: 1.62;
132
+ white-space: pre-wrap; word-break: break-word;
133
+ color: var(--texto); margin: 0;
134
+ }
135
+ .saida a { color: var(--azul); }
136
+
137
+ /* As cores ANSI, traduzidas. Os números são os do lib/ui.js. */
138
+ .c45 { color: var(--azul); }
139
+ .c31 { color: var(--azul-fundo); }
140
+ .c254 { color: var(--texto); }
141
+ .c247 { color: var(--fraco); }
142
+ .c243 { color: var(--apagado); }
143
+ .c214 { color: var(--alerta); }
144
+ .c204 { color: var(--falha); }
145
+ .forte { font-weight: 650; }
146
+
147
+ .aviso-erro {
148
+ margin: 14px 0; padding: 10px 14px; line-height: 1.55;
149
+ color: var(--falha); background: rgba(251, 113, 133, .08);
150
+ border: 1px solid rgba(251, 113, 133, .26); border-radius: 10px;
151
+ }
152
+
153
+ /* ── barra de baixo ─────────────────────────────────────────────────────── */
154
+
155
+ #baixo {
156
+ border-top: 1px solid var(--borda);
157
+ background: rgba(10, 15, 22, .82);
158
+ backdrop-filter: blur(14px);
159
+ padding: 12px 22px 14px;
160
+ }
161
+
162
+ #pensando {
163
+ max-width: 860px; margin: 0 auto 9px;
164
+ display: flex; align-items: center; gap: 9px;
165
+ color: var(--azul); font-family: var(--mono); font-size: 12.5px;
166
+ }
167
+ .pulso {
168
+ width: 7px; height: 7px; border-radius: 50%; background: var(--azul);
169
+ animation: bater 1.15s ease-in-out infinite;
170
+ }
171
+ @keyframes bater {
172
+ 0%, 100% { opacity: .28; transform: scale(.72); }
173
+ 50% { opacity: 1; transform: scale(1); }
174
+ }
175
+
176
+ #forma {
177
+ max-width: 860px; margin: 0 auto;
178
+ display: flex; align-items: flex-end; gap: 9px;
179
+ background: var(--painel);
180
+ border: 1px solid var(--borda);
181
+ border-radius: 15px; padding: 8px 8px 8px 15px;
182
+ transition: border-color .16s, box-shadow .16s;
183
+ }
184
+ #forma:focus-within {
185
+ border-color: rgba(34, 211, 238, .45);
186
+ box-shadow: 0 0 0 3px rgba(34, 211, 238, .09);
187
+ }
188
+
189
+ #campo {
190
+ flex: 1; font: inherit; font-size: 14.5px; line-height: 1.55;
191
+ color: var(--texto); background: none; border: 0; outline: 0;
192
+ resize: none; max-height: 44vh; padding: 6px 0;
193
+ }
194
+ #campo::placeholder { color: var(--apagado); }
195
+
196
+ #forma button {
197
+ flex: 0 0 auto; width: 34px; height: 34px; border-radius: 10px;
198
+ display: grid; place-items: center; cursor: pointer; border: 0;
199
+ transition: background .16s, color .16s, opacity .16s;
200
+ }
201
+ #forma button svg { width: 18px; height: 18px; }
202
+ #mandar { background: var(--azul); color: #04252c; }
203
+ #mandar:hover { background: #67e8f9; }
204
+ #mandar:disabled { background: #16323d; color: #3f6472; cursor: default; }
205
+ #parar { background: rgba(251, 113, 133, .14); color: var(--falha); }
206
+ #parar:hover { background: rgba(251, 113, 133, .24); }
207
+
208
+ .dica {
209
+ max-width: 860px; margin: 8px auto 0;
210
+ color: var(--apagado); font-size: 11.5px; text-align: center;
211
+ }
212
+
213
+ /* ── arrastar arquivo ───────────────────────────────────────────────────── */
214
+
215
+ #alvo-solto {
216
+ position: fixed; inset: 0; display: none; place-items: center;
217
+ background: rgba(6, 10, 16, .82); backdrop-filter: blur(3px); z-index: 40;
218
+ }
219
+ body.soltando #alvo-solto { display: grid; }
220
+ #alvo-solto > div {
221
+ border: 2px dashed rgba(34, 211, 238, .55); border-radius: 18px;
222
+ padding: 44px 62px; text-align: center; color: var(--azul);
223
+ display: grid; gap: 6px;
224
+ }
225
+ #alvo-solto span { color: var(--fraco); font-size: 13px; }
226
+
227
+ @media (max-width: 620px) {
228
+ .estado #pasta { display: none; }
229
+ #conversa, #forma, #pensando, .dica { max-width: none; }
230
+ }
package/app/app.js ADDED
@@ -0,0 +1,273 @@
1
+ /* A janela conversando com o PrimoCode.
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.
5
+ *
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.
10
+ */
11
+ (function () {
12
+ "use strict";
13
+
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") || "";
19
+ try {
20
+ if (CHAVE) sessionStorage.setItem("primo-chave", CHAVE);
21
+ else CHAVE = sessionStorage.getItem("primo-chave") || "";
22
+ } catch (e) { /* sem sessionStorage: vale só esta abertura */ }
23
+ if (location.search) history.replaceState(null, "", location.pathname);
24
+
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");
32
+ var ocupado = false;
33
+
34
+ function pedir(rota, corpo) {
35
+ return fetch(rota, {
36
+ method: corpo === undefined ? "GET" : "POST",
37
+ headers: { "Content-Type": "application/json", "X-Primo-Chave": CHAVE },
38
+ body: corpo === undefined ? undefined : JSON.stringify(corpo),
39
+ });
40
+ }
41
+
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;
48
+ }
49
+ function acompanhar(estava) {
50
+ if (estava) rolo.scrollTop = rolo.scrollHeight;
51
+ }
52
+
53
+ function limparBoasVindas() {
54
+ var b = conversa.querySelector(".boas-vindas");
55
+ if (b) b.remove();
56
+ }
57
+
58
+ /* ── ANSI → HTML ───────────────────────────────────────────────────────
59
+ Só o que o PrimoCode realmente emite: SGR de cor 256 (38;5;N), negrito e
60
+ reset. O resto dos códigos (mover cursor, apagar linha) é desenho de
61
+ terminal e é descartado — na página quem rola é o navegador. */
62
+ var ANSI = /\x1b\[([0-9;?]*)([a-zA-Z])/g;
63
+
64
+ function fugir(s) {
65
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
66
+ }
67
+
68
+ function paraHtml(texto, estado) {
69
+ var fora = "";
70
+ var ultimo = 0;
71
+ var m;
72
+ ANSI.lastIndex = 0;
73
+
74
+ function abrir() {
75
+ var classes = [];
76
+ if (estado.cor) classes.push("c" + estado.cor);
77
+ if (estado.forte) classes.push("forte");
78
+ return classes.length ? '<span class="' + classes.join(" ") + '">' : "";
79
+ }
80
+ function fechar() {
81
+ return (estado.cor || estado.forte) ? "</span>" : "";
82
+ }
83
+
84
+ while ((m = ANSI.exec(texto)) !== null) {
85
+ var pedaco = texto.slice(ultimo, m.index);
86
+ if (pedaco) fora += abrir() + fugir(pedaco) + fechar();
87
+ ultimo = m.index + m[0].length;
88
+ if (m[2] !== "m") continue; // não é cor: descarta
89
+ var partes = m[1].split(";");
90
+ for (var i = 0; i < partes.length; i++) {
91
+ var n = partes[i];
92
+ if (n === "" || n === "0") { estado.cor = null; estado.forte = false; }
93
+ else if (n === "1") estado.forte = true;
94
+ else if (n === "38" && partes[i + 1] === "5") { estado.cor = partes[i + 2]; i += 2; }
95
+ else if (n === "39") estado.cor = null;
96
+ else if (n === "22") estado.forte = false;
97
+ }
98
+ }
99
+ var resto = texto.slice(ultimo);
100
+ if (resto) fora += abrir() + fugir(resto) + fechar();
101
+ return fora;
102
+ }
103
+
104
+ /* O estado de cor atravessa os pedaços: o Node corta a saída onde der, e um
105
+ "\x1b[38;5;45m" pode chegar num pedaço e o texto colorido no seguinte.
106
+ Guardando o estado fora da função, a cor continua do outro lado do corte. */
107
+ var corrente = { cor: null, forte: false };
108
+ var bloco = null;
109
+
110
+ function escreverSaida(texto) {
111
+ var estava = noFim();
112
+ limparBoasVindas();
113
+ if (!bloco) {
114
+ bloco = document.createElement("pre");
115
+ bloco.className = "saida";
116
+ conversa.appendChild(bloco);
117
+ }
118
+ bloco.insertAdjacentHTML("beforeend", paraHtml(texto, corrente));
119
+ acompanhar(estava);
120
+ }
121
+
122
+ function escreverVoce(texto) {
123
+ var estava = noFim();
124
+ limparBoasVindas();
125
+ var linha = document.createElement("div");
126
+ linha.className = "voce";
127
+ var caixa = document.createElement("div");
128
+ caixa.textContent = texto;
129
+ linha.appendChild(caixa);
130
+ conversa.appendChild(linha);
131
+ bloco = null; // a próxima saída começa bloco novo
132
+ corrente = { cor: null, forte: false };
133
+ acompanhar(true);
134
+ void estava;
135
+ }
136
+
137
+ function escreverErro(texto) {
138
+ var estava = noFim();
139
+ var d = document.createElement("div");
140
+ d.className = "aviso-erro";
141
+ d.textContent = texto;
142
+ conversa.appendChild(d);
143
+ bloco = null;
144
+ acompanhar(estava);
145
+ }
146
+
147
+ /* ── estado ────────────────────────────────────────────────────────────── */
148
+
149
+ function pintarEstado(e) {
150
+ if (!e) return;
151
+ if (e.versao) document.getElementById("versao").textContent = "v" + e.versao;
152
+ document.getElementById("pasta").textContent = e.pasta || "";
153
+ document.getElementById("modo").textContent = e.modo || "";
154
+ document.getElementById("esforco").textContent = e.esforco || "";
155
+ document.title = (e.projeto ? e.projeto + " — " : "") + "PrimoCode";
156
+ }
157
+
158
+ function pintarOcupado(v) {
159
+ ocupado = !!v;
160
+ pensando.hidden = !ocupado;
161
+ parar.hidden = !ocupado;
162
+ mandar.hidden = ocupado;
163
+ campo.placeholder = ocupado
164
+ ? "ele está trabalhando — pode ir escrevendo o próximo"
165
+ : "Peça alguma coisa… (Enter manda, Shift+Enter quebra linha)";
166
+ }
167
+
168
+ /* ── o fluxo ───────────────────────────────────────────────────────────
169
+ EventSource reconecta sozinho quando a conexão cai, e o servidor manda o
170
+ que já saiu ao ligar — então uma queda de rede local não deixa buraco na
171
+ conversa. */
172
+ function ligar() {
173
+ var fonte = new EventSource("/api/fluxo?k=" + encodeURIComponent(CHAVE));
174
+ fonte.addEventListener("saida", function (ev) { escreverSaida(JSON.parse(ev.data)); });
175
+ fonte.addEventListener("voce", function (ev) { escreverVoce(JSON.parse(ev.data)); });
176
+ fonte.addEventListener("erro", function (ev) { escreverErro(JSON.parse(ev.data)); });
177
+ fonte.addEventListener("estado", function (ev) { pintarEstado(JSON.parse(ev.data)); });
178
+ fonte.addEventListener("ocupado", function (ev) { pintarOcupado(JSON.parse(ev.data)); });
179
+ fonte.onerror = function () { /* o EventSource se vira sozinho */ };
180
+ }
181
+
182
+ /* ── mandar ────────────────────────────────────────────────────────────── */
183
+
184
+ function enviar(texto) {
185
+ texto = (texto || "").trim();
186
+ if (!texto) return;
187
+ pedir("/api/enviar", { texto: texto });
188
+ campo.value = "";
189
+ ajustarAltura();
190
+ }
191
+
192
+ function ajustarAltura() {
193
+ campo.style.height = "auto";
194
+ campo.style.height = Math.min(campo.scrollHeight, window.innerHeight * 0.44) + "px";
195
+ }
196
+
197
+ forma.addEventListener("submit", function (ev) {
198
+ ev.preventDefault();
199
+ enviar(campo.value);
200
+ });
201
+
202
+ campo.addEventListener("input", ajustarAltura);
203
+ campo.addEventListener("keydown", function (ev) {
204
+ if (ev.key === "Enter" && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
205
+ ev.preventDefault();
206
+ enviar(campo.value);
207
+ }
208
+ });
209
+
210
+ parar.addEventListener("click", function () { pedir("/api/parar", {}); });
211
+
212
+ document.addEventListener("keydown", function (ev) {
213
+ if (ev.key === "Escape" && ocupado) { pedir("/api/parar", {}); return; }
214
+ // Qualquer tecla de conteúdo devolve o foco ao campo: numa janela de chat
215
+ // não se clica para escrever.
216
+ if (ev.key.length === 1 && !ev.metaKey && !ev.ctrlKey && document.activeElement !== campo) {
217
+ campo.focus();
218
+ }
219
+ });
220
+
221
+ document.querySelectorAll(".atalhos button").forEach(function (b) {
222
+ b.addEventListener("click", function () { enviar(b.dataset.manda); });
223
+ });
224
+
225
+ /* ── arrastar arquivo ──────────────────────────────────────────────────
226
+ "Eu quero que eu consiga arrastar os meus vídeos pra dentro do terminal,
227
+ e também lá no desktop."
228
+
229
+ O navegador não entrega o CAMINHO de um arquivo solto, por segurança — só
230
+ o conteúdo. Mas o PrimoCode roda na mesma máquina: o que ele precisa é do
231
+ caminho, e o nome sozinho não basta.
232
+
233
+ Duas portas, então. Soltar o arquivo escreve o nome no campo com uma
234
+ frase que o agente entende ("o arquivo X, que está em..."), e soltar TEXTO
235
+ — que é o que o Finder e o Explorer dão ao arrastar com a tecla de atalho,
236
+ e o que todo terminal recebe — entra como caminho de verdade. */
237
+ var conta = 0;
238
+ window.addEventListener("dragenter", function (ev) {
239
+ ev.preventDefault(); conta++; document.body.classList.add("soltando");
240
+ });
241
+ window.addEventListener("dragover", function (ev) { ev.preventDefault(); });
242
+ window.addEventListener("dragleave", function () {
243
+ if (--conta <= 0) { conta = 0; document.body.classList.remove("soltando"); }
244
+ });
245
+ window.addEventListener("drop", function (ev) {
246
+ ev.preventDefault();
247
+ conta = 0;
248
+ document.body.classList.remove("soltando");
249
+
250
+ var texto = "";
251
+ try { texto = ev.dataTransfer.getData("text/plain") || ""; } catch (e) {}
252
+ if (!texto && ev.dataTransfer.files && ev.dataTransfer.files.length) {
253
+ texto = Array.prototype.map.call(ev.dataTransfer.files, function (f) {
254
+ // Sem caminho: o nome, e o aviso de onde procurar. É o que dá para
255
+ // dizer com honestidade daqui.
256
+ return f.name;
257
+ }).join(" ");
258
+ if (texto) {
259
+ texto = "o arquivo " + texto + " (procure na pasta de Downloads ou na "
260
+ + "Área de Trabalho; se não achar, me peça o caminho completo)";
261
+ }
262
+ }
263
+ if (!texto) return;
264
+ campo.value = (campo.value ? campo.value.trimEnd() + " " : "") + texto + " ";
265
+ campo.focus();
266
+ ajustarAltura();
267
+ });
268
+
269
+ pedir("/api/estado").then(function (r) { return r.json(); }).then(pintarEstado)
270
+ .catch(function () {});
271
+ ligar();
272
+ campo.focus();
273
+ })();
package/app/icone.svg ADDED
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="7" fill="#0a0f16"/>
3
+ <path d="M8 10.5 13 16l-5 5.5" fill="none" stroke="#22d3ee" stroke-width="2.6"
4
+ stroke-linecap="round" stroke-linejoin="round"/>
5
+ <path d="M16.5 22H24" fill="none" stroke="#22d3ee" stroke-width="2.6" stroke-linecap="round"/>
6
+ </svg>
package/app/index.html ADDED
@@ -0,0 +1,68 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>PrimoCode</title>
7
+ <link rel="stylesheet" href="/app.css">
8
+ <link rel="icon" href="/icone.svg">
9
+ </head>
10
+ <body>
11
+
12
+ <header id="topo">
13
+ <div class="marca">
14
+ <svg class="logo" viewBox="0 0 24 24" aria-hidden="true">
15
+ <path d="M4 6.5 9 12l-5 5.5" fill="none" stroke="currentColor" stroke-width="2.1"
16
+ stroke-linecap="round" stroke-linejoin="round"/>
17
+ <path d="M12.5 18h7.5" fill="none" stroke="currentColor" stroke-width="2.1"
18
+ stroke-linecap="round"/>
19
+ </svg>
20
+ <span class="nome">PrimoCode</span>
21
+ <span class="versao" id="versao"></span>
22
+ </div>
23
+ <div class="estado">
24
+ <span class="chip" id="pasta" title="pasta de trabalho"></span>
25
+ <span class="chip" id="modo"></span>
26
+ <span class="chip" id="esforco"></span>
27
+ </div>
28
+ </header>
29
+
30
+ <main id="rolo">
31
+ <div id="conversa">
32
+ <div class="boas-vindas">
33
+ <h1>Oi. O que a gente vai construir?</h1>
34
+ <p>Escreva do jeito que você falaria. Ele lê a pasta, escreve o código,
35
+ roda o que precisar e conta o que fez.</p>
36
+ <div class="atalhos">
37
+ <button data-manda="/tudo">o que ele sabe fazer</button>
38
+ <button data-manda="/project">meus projetos</button>
39
+ <button data-manda="/effort">esforço</button>
40
+ <button data-manda="/help">comandos</button>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </main>
45
+
46
+ <footer id="baixo">
47
+ <div id="pensando" hidden><span class="pulso"></span><span id="pensando-txt">trabalhando</span></div>
48
+ <form id="forma">
49
+ <textarea id="campo" rows="1" placeholder="Peça alguma coisa… (Enter manda, Shift+Enter quebra linha)"
50
+ autocomplete="off" spellcheck="false"></textarea>
51
+ <button type="submit" id="mandar" title="Enviar (Enter)" aria-label="Enviar">
52
+ <svg viewBox="0 0 24 24" aria-hidden="true">
53
+ <path d="M4 12h13m0 0-5-5m5 5-5 5" fill="none" stroke="currentColor"
54
+ stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/>
55
+ </svg>
56
+ </button>
57
+ <button type="button" id="parar" title="Parar (Esc)" hidden aria-label="Parar">
58
+ <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="7" y="7" width="10" height="10" rx="2" fill="currentColor"/></svg>
59
+ </button>
60
+ </form>
61
+ <div class="dica" id="dica">arraste um vídeo ou arquivo aqui para o PrimoCode usar</div>
62
+ </footer>
63
+
64
+ <div id="alvo-solto"><div><strong>Solte aqui</strong><span>o caminho entra no pedido</span></div></div>
65
+
66
+ <script src="/app.js"></script>
67
+ </body>
68
+ </html>
package/bin/primocode.js CHANGED
@@ -25,6 +25,7 @@ const pasta = require('../lib/pasta');
25
25
  const catalogo = require('../lib/catalogo');
26
26
  const effort = require('../lib/effort');
27
27
  const memoria = require('../lib/memoria-conversa');
28
+ const appLib = require('../lib/app');
28
29
 
29
30
  // Rede de segurança de último recurso: sem isto, uma exceção fora de um
30
31
  // try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
@@ -61,6 +62,10 @@ const state = {
61
62
  engine: 'server', // 'server' (OpenRouter/Groq) | 'claude-code' (/claude)
62
63
  };
63
64
 
65
+ // A janela do /app, quando aberta. Uma por sessão: o segundo /app traz a que
66
+ // existe para a frente em vez de subir outro servidor ao lado.
67
+ let janelaAberta = null;
68
+
64
69
  const HELP = `
65
70
  ${c.bold('Comandos')}
66
71
  ${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
@@ -75,6 +80,7 @@ ${c.bold('Comandos')}
75
80
  ${c.brand('/read')} <arquivo> mostra um arquivo
76
81
  ${c.brand('/run')} <comando> executa no shell do projeto
77
82
  ${c.brand('/open')} <arquivo|url> abre no navegador
83
+ ${c.brand('/app')} abre a janela do PrimoCode (chat, sem terminal)
78
84
  ${c.brand('/status')} checa o servidor
79
85
  ${c.brand('/name')} <nome> como o Primo te chama
80
86
  ${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
@@ -115,6 +121,7 @@ const SLASH_COMANDOS = [
115
121
  ['/read', 'mostra um arquivo'],
116
122
  ['/run', 'roda um comando'],
117
123
  ['/open', 'abre no navegador'],
124
+ ['/app', 'abre a janela do PrimoCode'],
118
125
  ['/status', 'checa o servidor'],
119
126
  ['/name', 'como ele te chama'],
120
127
  ['/mascot', 'primo | fox | cat'],
@@ -925,6 +932,51 @@ async function handleSlash(text) {
925
932
  break;
926
933
  }
927
934
 
935
+ /* A janela. "Se mandar /app pra ele, ele abre o aplicativo — para o
936
+ Mac e o Windows, já instalado junto com o npm, com as cores do
937
+ PrimoCode. Um chat estilo terminal para quem não quer o terminal."
938
+
939
+ Uma janela por sessão: mandar /app de novo traz a que já existe
940
+ para a frente em vez de abrir outra do lado. */
941
+ case 'app': {
942
+ if (janelaAberta) {
943
+ // Já existe: reabre no MESMO endereço. O navegador traz a
944
+ // janela daquela URL para a frente em vez de fazer outra.
945
+ await appLib.abrirJanela(janelaAberta.url);
946
+ console.log(c.muted(' A janela já estava aberta — trouxe para a frente.'));
947
+ break;
948
+ }
949
+ const sp = new Spinner('abrindo a janela').start();
950
+ try {
951
+ janelaAberta = await appLib.abrir({
952
+ versao: VERSION,
953
+ enviar: (t) => sendPrompt(t),
954
+ barra: (t) => handleSlash(t),
955
+ interromper,
956
+ estado: () => ({
957
+ pasta: state.workDir ? shortPath(state.workDir) : null,
958
+ projeto: state.currentProject,
959
+ modo: state.mode,
960
+ esforco: state.config.effort,
961
+ }),
962
+ });
963
+ sp.stop();
964
+ const j = janelaAberta.janela || {};
965
+ console.log(c.ok(' Janela aberta.')
966
+ + c.muted(j.modo === 'janela' ? ` (${j.navegador})`
967
+ : j.modo === 'aba' ? ' (aba do navegador padrão)' : ''));
968
+ console.log(c.dim(` ${janelaAberta.url}`));
969
+ if (j.modo === 'nenhum') {
970
+ console.log(c.warn(' Não consegui abrir sozinho — copie o endereço acima no navegador.'));
971
+ }
972
+ console.log(c.muted(' O terminal continua funcionando: os dois mostram a mesma coisa.'));
973
+ } catch (e) {
974
+ sp.stop();
975
+ console.log(c.err(` Não subiu: ${e.message}`));
976
+ }
977
+ break;
978
+ }
979
+
928
980
  case 'uninstall': {
929
981
  if (await confirm('Remover o PrimoCode? Seus projetos em ~/primocode NÃO serão apagados.')) {
930
982
  await tools.uninstall();