primocode 8.25.0 → 8.27.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.
- package/app/app.css +230 -0
- package/app/app.js +273 -0
- package/app/icone.svg +6 -0
- package/app/index.html +68 -0
- package/bin/primocode.js +52 -0
- package/lib/app.js +334 -0
- package/package.json +3 -2
- package/studio/ferramentas/00-protocolo.md +2 -1
- package/studio/ferramentas/01-modelos.md +3 -2
- package/studio/ferramentas/corte.md +81 -0
- package/studio/server.py +9 -3
- package/studio/web/js/ferramentas.js +2 -2
- package/studio/web/js/render.js +219 -5
- package/studio/web/render.css +277 -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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();
|