primocode 7.11.0 → 7.12.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/lib/streaming.js +236 -67
- package/package.json +1 -1
package/lib/streaming.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* O agente age no desktop do usuário (clica, digita, abre janela), mas rápido
|
|
5
5
|
* demais para acompanhar a olho nu — e às vezes numa janela que não está na
|
|
6
|
-
* frente. O /streaming liga um servidorzinho HTTP local que
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* frente. O /streaming liga um servidorzinho HTTP local que abre um site com
|
|
7
|
+
* a cara do PrimoCode: mascote + nome no cabeçalho e, abaixo, uma "janela"
|
|
8
|
+
* com ABAS — "Ao vivo" mostra a tela agora, e cada app/janela em que o agente
|
|
9
|
+
* mexeu vira uma aba própria, com os passos e as fotos daquele app. Dá para
|
|
10
|
+
* voltar e rever o que ele já fez, passo a passo.
|
|
9
11
|
*
|
|
10
12
|
* Zero dependências: http nativo, uma página HTML embutida que consulta
|
|
11
13
|
* /state a cada 700ms. Só escuta em 127.0.0.1 — nada disso sai da máquina.
|
|
@@ -20,8 +22,11 @@ const os = require('os');
|
|
|
20
22
|
const desktop = require('./desktop');
|
|
21
23
|
|
|
22
24
|
const DIR = path.join(os.homedir(), '.primocode', 'streaming');
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
+
const MAX_LOG = 300;
|
|
26
|
+
// Fotos guardadas em disco para o "voltar no tempo" das abas. Além disso a
|
|
27
|
+
// pasta viraria um álbum infinito — as mais velhas saem.
|
|
28
|
+
const MAX_FRAMES = 80;
|
|
29
|
+
const MAX_ABAS = 12;
|
|
25
30
|
// Uma foto por ação no máximo a cada 600ms: digitar letra por letra não pode
|
|
26
31
|
// virar uma rajada de screenshots.
|
|
27
32
|
const INTERVALO_FOTO_MS = 600;
|
|
@@ -30,7 +35,10 @@ const state = {
|
|
|
30
35
|
on: false,
|
|
31
36
|
server: null,
|
|
32
37
|
url: '',
|
|
33
|
-
|
|
38
|
+
frames: [], // [{ id, hora, acao, aba }] — id é o nome do arquivo
|
|
39
|
+
proximoFrame: 1,
|
|
40
|
+
abas: [], // [{ id, nome, passos: 0 }] — "Ao vivo" é virtual, não entra aqui
|
|
41
|
+
abaAtual: null,
|
|
34
42
|
log: [],
|
|
35
43
|
erroFoto: null,
|
|
36
44
|
ultimaFoto: 0,
|
|
@@ -88,6 +96,33 @@ function descrever(name, args = {}) {
|
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
|
|
99
|
+
// ── Abas: uma por app/janela em que o agente mexeu ────────────────────────
|
|
100
|
+
// Abrir um app, focar uma janela ou abrir um site começa (ou reencontra) a
|
|
101
|
+
// aba daquele contexto. As ações seguintes caem nela — é o que deixa o
|
|
102
|
+
// "o que a IA já fez" navegável depois, em vez de um rolo único.
|
|
103
|
+
|
|
104
|
+
function nomeDeAba(name, args = {}) {
|
|
105
|
+
if (name === 'desktop_open_app') return String(args.app || args.name || '').trim();
|
|
106
|
+
if (name === 'desktop_focus') return String(args.window || args.title || '').trim();
|
|
107
|
+
if (name === 'browser_open') {
|
|
108
|
+
try { return new URL(String(args.url)).hostname || 'navegador'; } catch { return 'navegador'; }
|
|
109
|
+
}
|
|
110
|
+
if (args && args.window) return String(args.window).trim();
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function entrarNaAba(nome) {
|
|
115
|
+
const alvo = corta(nome, 24);
|
|
116
|
+
if (!alvo) return;
|
|
117
|
+
let aba = state.abas.find((a) => a.nome.toLowerCase() === alvo.toLowerCase());
|
|
118
|
+
if (!aba) {
|
|
119
|
+
aba = { id: state.abas.length + 1, nome: alvo, passos: 0 };
|
|
120
|
+
state.abas.push(aba);
|
|
121
|
+
if (state.abas.length > MAX_ABAS) state.abas.shift();
|
|
122
|
+
}
|
|
123
|
+
state.abaAtual = aba;
|
|
124
|
+
}
|
|
125
|
+
|
|
91
126
|
/**
|
|
92
127
|
* Chamada pelo loop do agente depois de CADA tool. Quando o streaming está
|
|
93
128
|
* desligado é um no-op — o caminho normal não paga nada por isto.
|
|
@@ -95,99 +130,220 @@ function descrever(name, args = {}) {
|
|
|
95
130
|
function step(name, args, result) {
|
|
96
131
|
if (!state.on) return;
|
|
97
132
|
|
|
133
|
+
const troca = nomeDeAba(name, args || {});
|
|
134
|
+
if (troca) entrarNaAba(troca);
|
|
135
|
+
if (!state.abaAtual && ACOES_NA_TELA.has(name)) entrarNaAba('Tela');
|
|
136
|
+
|
|
98
137
|
state.passo++;
|
|
99
|
-
state.
|
|
138
|
+
if (state.abaAtual) state.abaAtual.passos++;
|
|
139
|
+
|
|
140
|
+
const entrada = {
|
|
100
141
|
n: state.passo,
|
|
101
142
|
hora: new Date().toTimeString().slice(0, 8),
|
|
102
143
|
acao: descrever(name, args || {}),
|
|
103
144
|
ok: !(result && result.ok === false),
|
|
104
145
|
erro: result && result.ok === false ? corta(result.error, 120) : undefined,
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
aba: state.abaAtual ? state.abaAtual.id : null,
|
|
147
|
+
};
|
|
107
148
|
|
|
108
|
-
if (ACOES_NA_TELA.has(name))
|
|
149
|
+
if (ACOES_NA_TELA.has(name)) {
|
|
150
|
+
const frame = capturar(entrada);
|
|
151
|
+
if (frame) entrada.frame = frame;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
state.log.push(entrada);
|
|
155
|
+
if (state.log.length > MAX_LOG) state.log.splice(0, state.log.length - MAX_LOG);
|
|
109
156
|
}
|
|
110
157
|
|
|
111
|
-
function capturar() {
|
|
158
|
+
function capturar(entrada) {
|
|
112
159
|
const agora = Date.now();
|
|
113
|
-
if (agora - state.ultimaFoto < INTERVALO_FOTO_MS) return;
|
|
160
|
+
if (agora - state.ultimaFoto < INTERVALO_FOTO_MS) return null;
|
|
114
161
|
state.ultimaFoto = agora;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
162
|
+
|
|
163
|
+
const id = state.proximoFrame;
|
|
164
|
+
const arquivo = path.join(DIR, `frame-${id}.png`);
|
|
165
|
+
const r = desktop.screenshot(arquivo);
|
|
166
|
+
if (!r.ok) { if (!state.erroFoto) state.erroFoto = r.error; return null; }
|
|
167
|
+
|
|
168
|
+
state.erroFoto = null;
|
|
169
|
+
state.proximoFrame++;
|
|
170
|
+
state.frames.push({ id, hora: entrada.hora, acao: entrada.acao, aba: entrada.aba });
|
|
171
|
+
while (state.frames.length > MAX_FRAMES) {
|
|
172
|
+
const velho = state.frames.shift();
|
|
173
|
+
try { fs.unlinkSync(path.join(DIR, `frame-${velho.id}.png`)); } catch {}
|
|
174
|
+
}
|
|
175
|
+
return id;
|
|
118
176
|
}
|
|
119
177
|
|
|
120
178
|
// ── Página ────────────────────────────────────────────────────────────────
|
|
179
|
+
// O desenho combinado: mascote + PrimoCode no cabeçalho; abaixo, uma "janela"
|
|
180
|
+
// de borda branca com as três bolinhas e as ABAS; dentro, a tela que a IA
|
|
181
|
+
// está usando (ou usou, nas abas antigas), com os passos daquela aba embaixo.
|
|
182
|
+
|
|
183
|
+
// O mascote do CLI (▐▛███▜▌ …) redesenhado em SVG, célula a célula, na mesma
|
|
184
|
+
// paleta: bloco cheio na cor viva, contorno na apagada.
|
|
185
|
+
const MASCOTE_SVG = `<svg viewBox="0 0 9 3.6" aria-hidden="true">
|
|
186
|
+
<g fill="#0891b2"><rect x="1" y="0" width="1" height="1"/><rect x="7" y="0" width="1" height="1"/><rect x="0" y="1" width="1" height="1" opacity=".7"/><rect x="8" y="1" width="1" height="1" opacity=".7"/><rect x="1" y="1" width="1" height="1"/><rect x="7" y="1" width="1" height="1"/></g>
|
|
187
|
+
<g fill="#22d3ee"><rect x="2" y="0" width="5" height="1" rx=".12"/><rect x="2" y="1" width="5" height="1"/><rect x="1.9" y="2.2" width=".9" height=".5" rx=".1"/><rect x="3.4" y="2.2" width=".9" height=".5" rx=".1"/><rect x="4.9" y="2.2" width=".9" height=".5" rx=".1"/><rect x="6.4" y="2.2" width=".9" height=".5" rx=".1"/></g>
|
|
188
|
+
<g fill="#0b1220"><rect x="3" y=".35" width=".8" height=".5" rx=".1"/><rect x="5.2" y=".35" width=".8" height=".5" rx=".1"/><rect x="3.9" y="1.25" width="1.2" height=".28" rx=".14"/></g>
|
|
189
|
+
</svg>`;
|
|
190
|
+
|
|
121
191
|
const PAGINA = `<!doctype html>
|
|
122
192
|
<html lang="pt-BR"><head><meta charset="utf-8">
|
|
123
193
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
124
194
|
<title>PrimoCode · Ao vivo</title>
|
|
125
195
|
<style>
|
|
126
196
|
* { box-sizing: border-box; margin: 0; }
|
|
127
|
-
body { background: #
|
|
128
|
-
|
|
197
|
+
body { background: #17181a; color: #e2e8f0; font: 14px/1.5 ui-monospace, 'Cascadia Code', Consolas, monospace; min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 18px 14px 26px; }
|
|
198
|
+
|
|
199
|
+
/* Cabeçalho: mascote + nome */
|
|
200
|
+
header { display: flex; align-items: center; gap: 16px; padding: 6px 0 18px; }
|
|
201
|
+
header .mascote { width: 72px; height: 30px; transform: rotate(-8deg); }
|
|
202
|
+
header .mascote svg { width: 100%; height: 100%; display: block; }
|
|
203
|
+
header h1 { font-size: 34px; font-weight: 700; letter-spacing: .5px; }
|
|
204
|
+
header h1 b { color: #22d3ee; font-weight: 700; }
|
|
205
|
+
header .vivo { margin-left: 14px; display: inline-flex; align-items: center; gap: 7px; color: #64748b; font-size: 12px; }
|
|
129
206
|
.dot { width: 9px; height: 9px; border-radius: 50%; background: #22d3ee; animation: pulsa 1.4s infinite; }
|
|
130
207
|
@keyframes pulsa { 50% { opacity: .25 } }
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
208
|
+
|
|
209
|
+
/* A "janela" */
|
|
210
|
+
#janela { width: min(1060px, 100%); background: #000; border: 4px solid #f4f4f4; border-radius: 26px; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 18px 60px rgba(0,0,0,.55); }
|
|
211
|
+
#barra { display: flex; align-items: center; gap: 8px; background: #232323; padding: 10px 16px; border-bottom: 1px solid #101010; }
|
|
212
|
+
#barra .b { width: 13px; height: 13px; border-radius: 50%; flex: none; }
|
|
213
|
+
#barra .b.r { background: #e5484d; } #barra .b.y { background: #f5d90a; } #barra .b.g { background: #46d160; }
|
|
214
|
+
|
|
215
|
+
#abas { display: flex; gap: 4px; margin-left: 14px; overflow-x: auto; scrollbar-width: none; }
|
|
216
|
+
#abas::-webkit-scrollbar { display: none; }
|
|
217
|
+
.aba { border: 0; cursor: pointer; white-space: nowrap; font: inherit; font-size: 13px; padding: 5px 14px; border-radius: 9px 9px 0 0; background: transparent; color: #94a3b8; }
|
|
218
|
+
.aba:hover { color: #e2e8f0; }
|
|
219
|
+
.aba.ativa { background: #000; color: #22d3ee; }
|
|
220
|
+
.aba .qtd { color: #475569; font-size: 11px; margin-left: 6px; }
|
|
221
|
+
|
|
222
|
+
#tela { position: relative; min-height: 380px; display: flex; align-items: center; justify-content: center; background: #000; }
|
|
223
|
+
#tela img { width: 100%; max-height: 62vh; object-fit: contain; display: none; }
|
|
224
|
+
#aviso { color: #94a3b8; text-align: center; max-width: 52ch; padding: 70px 20px; font-size: 15px; }
|
|
225
|
+
#carimbo { position: absolute; left: 12px; bottom: 10px; background: rgba(11,18,32,.82); border: 1px solid #1e293b; color: #94a3b8; font-size: 12px; padding: 3px 10px; border-radius: 7px; display: none; max-width: 85%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
226
|
+
|
|
227
|
+
/* Filmstrip + passos da aba */
|
|
228
|
+
#rodape { background: #0b0c0e; border-top: 1px solid #1c1d20; }
|
|
229
|
+
#filme { display: flex; gap: 6px; padding: 8px 12px 2px; overflow-x: auto; }
|
|
230
|
+
.foto { flex: none; border: 1px solid #26272b; border-radius: 6px; background: #000; cursor: pointer; padding: 0; width: 84px; height: 50px; overflow: hidden; opacity: .75; }
|
|
231
|
+
.foto img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
232
|
+
.foto.ativa { border-color: #22d3ee; opacity: 1; }
|
|
233
|
+
#passos { max-height: 150px; overflow-y: auto; padding: 6px 4px 10px; }
|
|
234
|
+
.item { padding: 3px 14px; display: flex; gap: 8px; font-size: 13px; }
|
|
140
235
|
.item .n { color: #475569; min-width: 3ch; text-align: right; }
|
|
141
|
-
.item .ok { color: #22d3ee; }
|
|
142
|
-
.item .
|
|
143
|
-
.item .txt {
|
|
236
|
+
.item .ok { color: #22d3ee; } .item .falha { color: #fb7185; }
|
|
237
|
+
.item .txt { flex: 1; word-break: break-word; color: #cbd5e1; }
|
|
238
|
+
.item .txt .erro { color: #fb7185; font-size: 12px; display: block; }
|
|
144
239
|
.item .hora { color: #475569; font-size: 11px; }
|
|
145
|
-
.item
|
|
240
|
+
.item.temfoto { cursor: pointer; } .item.temfoto:hover .txt { color: #22d3ee; }
|
|
241
|
+
|
|
242
|
+
footer { color: #475569; font-size: 12px; padding-top: 14px; }
|
|
146
243
|
</style></head>
|
|
147
244
|
<body>
|
|
148
|
-
<header
|
|
149
|
-
<
|
|
245
|
+
<header>
|
|
246
|
+
<div class="mascote">${MASCOTE_SVG}</div>
|
|
247
|
+
<h1><b>Primo</b>Code</h1>
|
|
248
|
+
<span class="vivo"><span class="dot"></span><span id="st">conectando…</span></span>
|
|
249
|
+
</header>
|
|
250
|
+
|
|
251
|
+
<div id="janela">
|
|
252
|
+
<div id="barra">
|
|
253
|
+
<span class="b r"></span><span class="b y"></span><span class="b g"></span>
|
|
254
|
+
<nav id="abas"></nav>
|
|
255
|
+
</div>
|
|
150
256
|
<section id="tela">
|
|
151
|
-
<img id="frame" alt="tela
|
|
152
|
-
<div id="aviso">
|
|
257
|
+
<img id="frame" alt="tela que a IA está usando">
|
|
258
|
+
<div id="aviso">Tela que a IA está usando aparece aqui, em tempo real.<br><br>Dê uma instrução no terminal do PrimoCode — cada app em que ela mexer vira uma aba, e você pode voltar para ver o que ela já fez.</div>
|
|
259
|
+
<div id="carimbo"></div>
|
|
153
260
|
</section>
|
|
154
|
-
<
|
|
155
|
-
|
|
261
|
+
<div id="rodape">
|
|
262
|
+
<div id="filme"></div>
|
|
263
|
+
<div id="passos"></div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
<footer>só nesta máquina (127.0.0.1) · /streaming desliga</footer>
|
|
267
|
+
|
|
156
268
|
<script>
|
|
157
|
-
|
|
269
|
+
const VIVO = 0; // id da aba "Ao vivo"
|
|
270
|
+
let dados = { abas: [], frames: [], log: [] };
|
|
271
|
+
let abaSel = VIVO; // aba escolhida pelo usuário
|
|
272
|
+
let fotoSel = null; // frame fixado no filmstrip (null = mais recente)
|
|
273
|
+
let ultimoEstado = '';
|
|
274
|
+
|
|
275
|
+
function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
276
|
+
|
|
277
|
+
function framesDaAba(id) {
|
|
278
|
+
return id === VIVO ? dados.frames : dados.frames.filter((f) => f.aba === id);
|
|
279
|
+
}
|
|
280
|
+
function logDaAba(id) {
|
|
281
|
+
return id === VIVO ? dados.log : dados.log.filter((e) => e.aba === id);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function render() {
|
|
285
|
+
// Abas: "Ao vivo" + uma por app/janela
|
|
286
|
+
const nav = document.getElementById('abas');
|
|
287
|
+
const abas = [{ id: VIVO, nome: 'Ao vivo' }, ...dados.abas];
|
|
288
|
+
nav.innerHTML = abas.map((a) =>
|
|
289
|
+
'<button class="aba' + (a.id === abaSel ? ' ativa' : '') + '" data-id="' + a.id + '">'
|
|
290
|
+
+ (a.id === VIVO ? '● ' : '') + esc(a.nome)
|
|
291
|
+
+ (a.passos ? '<span class="qtd">' + a.passos + '</span>' : '')
|
|
292
|
+
+ '</button>').join('');
|
|
293
|
+
nav.querySelectorAll('.aba').forEach((b) => b.onclick = () => { abaSel = Number(b.dataset.id); fotoSel = null; render(); });
|
|
294
|
+
|
|
295
|
+
// Imagem: a fixada no filmstrip, senão a mais nova da aba
|
|
296
|
+
const fs = framesDaAba(abaSel);
|
|
297
|
+
const atual = fotoSel != null ? fs.find((f) => f.id === fotoSel) : fs[fs.length - 1];
|
|
298
|
+
const img = document.getElementById('frame');
|
|
299
|
+
const aviso = document.getElementById('aviso');
|
|
300
|
+
const carimbo = document.getElementById('carimbo');
|
|
301
|
+
if (atual) {
|
|
302
|
+
const src = '/frame?n=' + atual.id;
|
|
303
|
+
if (img.dataset.n !== String(atual.id)) { img.src = src; img.dataset.n = String(atual.id); }
|
|
304
|
+
img.style.display = 'block'; aviso.style.display = 'none';
|
|
305
|
+
carimbo.style.display = 'block';
|
|
306
|
+
carimbo.textContent = '#' + atual.id + ' · ' + atual.hora + ' · ' + atual.acao + (fotoSel != null ? ' (clique na miniatura de novo para voltar ao mais recente)' : '');
|
|
307
|
+
} else {
|
|
308
|
+
img.style.display = 'none'; aviso.style.display = 'block'; carimbo.style.display = 'none';
|
|
309
|
+
if (dados.erroFoto) aviso.innerHTML = 'Sem foto da tela: ' + esc(dados.erroFoto);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Filmstrip da aba (as últimas 14)
|
|
313
|
+
const filme = document.getElementById('filme');
|
|
314
|
+
const tira = fs.slice(-14);
|
|
315
|
+
filme.innerHTML = tira.map((f) =>
|
|
316
|
+
'<button class="foto' + ((atual && f.id === atual.id) ? ' ativa' : '') + '" data-id="' + f.id + '" title="' + esc(f.acao) + '"><img loading="lazy" src="/frame?n=' + f.id + '"></button>').join('');
|
|
317
|
+
filme.querySelectorAll('.foto').forEach((b) => b.onclick = () => {
|
|
318
|
+
const id = Number(b.dataset.id);
|
|
319
|
+
fotoSel = (fotoSel === id) ? null : id; // clicar de novo volta ao "mais recente"
|
|
320
|
+
render();
|
|
321
|
+
});
|
|
322
|
+
if (fotoSel == null) filme.scrollLeft = filme.scrollWidth;
|
|
323
|
+
|
|
324
|
+
// Passos da aba (mais novos primeiro)
|
|
325
|
+
const passos = logDaAba(abaSel).slice(-40).reverse();
|
|
326
|
+
document.getElementById('passos').innerHTML = passos.map((e) =>
|
|
327
|
+
'<div class="item' + (e.frame ? ' temfoto" data-frame="' + e.frame : '') + '">'
|
|
328
|
+
+ '<span class="n">' + e.n + '</span>'
|
|
329
|
+
+ '<span class="' + (e.ok ? 'ok' : 'falha') + '">' + (e.ok ? '✔' : '✗') + '</span>'
|
|
330
|
+
+ '<span class="txt">' + esc(e.acao) + (e.erro ? '<span class="erro">' + esc(e.erro) + '</span>' : '') + '</span>'
|
|
331
|
+
+ '<span class="hora">' + e.hora + '</span></div>').join('');
|
|
332
|
+
document.querySelectorAll('.item.temfoto').forEach((el) => el.onclick = () => { fotoSel = Number(el.dataset.frame); render(); });
|
|
333
|
+
}
|
|
334
|
+
|
|
158
335
|
async function tick() {
|
|
159
336
|
try {
|
|
160
337
|
const s = await (await fetch('/state')).json();
|
|
338
|
+
dados = s;
|
|
161
339
|
document.getElementById('st').textContent = 'ao vivo · porta ' + location.port;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const img = document.getElementById('frame');
|
|
165
|
-
img.src = '/frame?' + s.frame;
|
|
166
|
-
img.style.display = 'block';
|
|
167
|
-
document.getElementById('aviso').style.display = 'none';
|
|
168
|
-
}
|
|
169
|
-
if (s.erroFoto) {
|
|
170
|
-
document.getElementById('aviso').textContent = 'Sem foto da tela: ' + s.erroFoto;
|
|
171
|
-
}
|
|
172
|
-
const fim = s.log.length ? s.log[s.log.length - 1].n : -1;
|
|
173
|
-
if (fim !== ultimoPasso) {
|
|
174
|
-
ultimoPasso = fim;
|
|
175
|
-
const el = document.getElementById('log');
|
|
176
|
-
el.innerHTML = s.log.slice().reverse().map((e, i) =>
|
|
177
|
-
'<div class="item' + (i === 0 ? ' novo' : '') + '">'
|
|
178
|
-
+ '<span class="n">' + e.n + '</span>'
|
|
179
|
-
+ '<span class="' + (e.ok ? 'ok' : 'falha') + '">' + (e.ok ? '✔' : '✗') + '</span>'
|
|
180
|
-
+ '<span class="txt">' + esc(e.acao)
|
|
181
|
-
+ (e.erro ? '<span class="erro">' + esc(e.erro) + '</span>' : '')
|
|
182
|
-
+ '</span><span class="hora">' + e.hora + '</span></div>'
|
|
183
|
-
).join('');
|
|
184
|
-
}
|
|
340
|
+
const assinatura = JSON.stringify([s.frames.length, s.log.length, s.abas.length, s.erroFoto]);
|
|
341
|
+
if (assinatura !== ultimoEstado) { ultimoEstado = assinatura; render(); }
|
|
185
342
|
} catch (e) {
|
|
186
343
|
document.getElementById('st').textContent = 'reconectando…';
|
|
187
344
|
}
|
|
188
345
|
setTimeout(tick, 700);
|
|
189
346
|
}
|
|
190
|
-
function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
191
347
|
tick();
|
|
192
348
|
</script>
|
|
193
349
|
</body></html>`;
|
|
@@ -195,19 +351,32 @@ tick();
|
|
|
195
351
|
function start() {
|
|
196
352
|
if (state.on) return Promise.resolve(state.url);
|
|
197
353
|
try { fs.mkdirSync(DIR, { recursive: true }); } catch {}
|
|
354
|
+
// Fotos de sessões passadas não pertencem a esta linha do tempo.
|
|
355
|
+
try { for (const f of fs.readdirSync(DIR)) if (/^frame-\d+\.png$/.test(f)) fs.unlinkSync(path.join(DIR, f)); } catch {}
|
|
198
356
|
|
|
199
357
|
return new Promise((resolve, reject) => {
|
|
200
358
|
const server = http.createServer((req, res) => {
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
359
|
+
const u = new URL(req.url || '/', 'http://x');
|
|
360
|
+
if (u.pathname === '/state') {
|
|
203
361
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
204
|
-
res.end(JSON.stringify({
|
|
362
|
+
res.end(JSON.stringify({
|
|
363
|
+
erroFoto: state.erroFoto,
|
|
364
|
+
abas: state.abas,
|
|
365
|
+
frames: state.frames,
|
|
366
|
+
log: state.log.slice(-120),
|
|
367
|
+
}));
|
|
205
368
|
return;
|
|
206
369
|
}
|
|
207
|
-
if (
|
|
208
|
-
|
|
370
|
+
if (u.pathname === '/frame') {
|
|
371
|
+
// O id vem de fora: só serve o arquivo se for número — nada de
|
|
372
|
+
// path traversal por aqui.
|
|
373
|
+
const id = Math.round(Number(u.searchParams.get('n')));
|
|
374
|
+
const conhecido = Number.isFinite(id) && state.frames.some((f) => f.id === id);
|
|
375
|
+
if (!conhecido) { res.writeHead(404); res.end(); return; }
|
|
376
|
+
fs.readFile(path.join(DIR, `frame-${id}.png`), (err, buf) => {
|
|
209
377
|
if (err) { res.writeHead(404); res.end(); return; }
|
|
210
|
-
|
|
378
|
+
// Cada frame é imutável: o navegador pode guardar à vontade.
|
|
379
|
+
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'max-age=3600' });
|
|
211
380
|
res.end(buf);
|
|
212
381
|
});
|
|
213
382
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.12.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|