primocode 7.10.1 → 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/bin/primocode.js +30 -1
- package/lib/act.js +27 -0
- package/lib/catalogo.js +15 -9
- package/lib/desktop.js +578 -47
- package/lib/modo.js +5 -1
- package/lib/streaming.js +410 -0
- package/lib/tools.js +16 -0
- package/lib/ui.js +6 -3
- package/package.json +3 -3
package/lib/modo.js
CHANGED
|
@@ -30,12 +30,16 @@ const VERBOS = [
|
|
|
30
30
|
'implement[ae]r?', 'l[eê]i?a?r?', 'procur[ae]r?', 'busc[ae]r?', 'encontr[ae]r?',
|
|
31
31
|
'renome[ai]r?', 'copi[ae]r?', 'commit[ae]r?', 'deploy',
|
|
32
32
|
'sobe', 'suba', 'subir', 'empurr[ae]r?', 'mergea?r?',
|
|
33
|
+
// Interação com o computador: "clica no botão", "digita a senha",
|
|
34
|
+
// "pesquisa no google", "aperta enter", "arrasta o arquivo".
|
|
35
|
+
'clic[ao]r?', 'clique', 'digit[ae]r?', 'pesquis[ae]r?', 'apert[ae]r?',
|
|
36
|
+
'arrast[ae]r?', 'rol[ae]r?', 'minimiz[ae]r?', 'maximiz[ae]r?', 'fech[ae]r?',
|
|
33
37
|
];
|
|
34
38
|
const RE_VERBO = new RegExp(`(^|\\s)(${VERBOS.join('|')})\\b`, 'i');
|
|
35
39
|
|
|
36
40
|
// Coisas que só existem quando há trabalho de arquivo/máquina envolvido.
|
|
37
41
|
const RE_ARTEFATO = /\.(js|ts|tsx|jsx|html|css|json|md|py|sh|yml|yaml|txt|csv|env)\b|[\w-]+\/[\w-]/i;
|
|
38
|
-
const RE_FERRAMENTA = /\b(navegador|browser|desktop|terminal|shell|git|npm|node|deploy|servidor|localhost|porta \d+|screenshot|print da tela)\b/i;
|
|
42
|
+
const RE_FERRAMENTA = /\b(navegador|browser|desktop|terminal|shell|git|npm|node|deploy|servidor|localhost|porta \d+|screenshot|print da tela|mouse|teclado|janela|[áa]rea de trabalho|aplicativo|[íi]cone)\b/i;
|
|
39
43
|
const RE_PASTA = /\b(pasta|diret[óo]rio|arquivo|arquivos|reposit[óo]rio|repo|projeto)\b/i;
|
|
40
44
|
|
|
41
45
|
// Conversa: cumprimento, agradecimento, pergunta conceitual.
|
package/lib/streaming.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* streaming.js — o painel "ao vivo": ver o agente mexendo no computador.
|
|
3
|
+
*
|
|
4
|
+
* O agente age no desktop do usuário (clica, digita, abre janela), mas rápido
|
|
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 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.
|
|
11
|
+
*
|
|
12
|
+
* Zero dependências: http nativo, uma página HTML embutida que consulta
|
|
13
|
+
* /state a cada 700ms. Só escuta em 127.0.0.1 — nada disso sai da máquina.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const http = require('http');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const desktop = require('./desktop');
|
|
23
|
+
|
|
24
|
+
const DIR = path.join(os.homedir(), '.primocode', 'streaming');
|
|
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;
|
|
30
|
+
// Uma foto por ação no máximo a cada 600ms: digitar letra por letra não pode
|
|
31
|
+
// virar uma rajada de screenshots.
|
|
32
|
+
const INTERVALO_FOTO_MS = 600;
|
|
33
|
+
|
|
34
|
+
const state = {
|
|
35
|
+
on: false,
|
|
36
|
+
server: null,
|
|
37
|
+
url: '',
|
|
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,
|
|
42
|
+
log: [],
|
|
43
|
+
erroFoto: null,
|
|
44
|
+
ultimaFoto: 0,
|
|
45
|
+
passo: 0,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Ações cujo efeito aparece NA TELA — são as que merecem uma foto nova.
|
|
49
|
+
const ACOES_NA_TELA = new Set([
|
|
50
|
+
'desktop_click', 'desktop_double_click', 'desktop_click_element',
|
|
51
|
+
'desktop_move', 'desktop_drag', 'desktop_type', 'desktop_key',
|
|
52
|
+
'desktop_scroll', 'desktop_open_app', 'desktop_focus', 'desktop_screenshot',
|
|
53
|
+
'browser_open', 'browser_click', 'browser_type', 'browser_key',
|
|
54
|
+
'browser_scroll',
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
function isOn() { return state.on; }
|
|
58
|
+
function url() { return state.url; }
|
|
59
|
+
|
|
60
|
+
const corta = (s, n) => { const t = String(s ?? ''); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
|
61
|
+
|
|
62
|
+
// A frase humana de cada ação — é o que aparece na linha do tempo.
|
|
63
|
+
function descrever(name, args = {}) {
|
|
64
|
+
switch (name) {
|
|
65
|
+
case 'desktop_click': return `clique em (${args.x}, ${args.y})`;
|
|
66
|
+
case 'desktop_double_click': return `clique duplo em (${args.x}, ${args.y})`;
|
|
67
|
+
case 'desktop_click_element': return `clique em “${corta(args.name, 40)}”${args.double ? ' (duplo)' : ''}`;
|
|
68
|
+
case 'desktop_move': return `mouse para (${args.x}, ${args.y})`;
|
|
69
|
+
case 'desktop_drag': return `arrastou (${args.from_x}, ${args.from_y}) → (${args.to_x}, ${args.to_y})`;
|
|
70
|
+
case 'desktop_type': return `digitou “${corta(args.text, 40)}”`;
|
|
71
|
+
case 'desktop_key': return `tecla ${args.key}`;
|
|
72
|
+
case 'desktop_scroll': return `rolou a tela (${args.dy ?? 300})`;
|
|
73
|
+
case 'desktop_open_app': return `abriu ${corta(args.app || args.name, 40)}`;
|
|
74
|
+
case 'desktop_focus': return `focou a janela “${corta(args.window || args.title, 40)}”`;
|
|
75
|
+
case 'desktop_windows': return 'listou as janelas abertas';
|
|
76
|
+
case 'desktop_elements': return `leu os elementos${args.window ? ` de “${corta(args.window, 30)}”` : ' da janela em foco'}`;
|
|
77
|
+
case 'desktop_screenshot': return 'tirou um print da tela';
|
|
78
|
+
case 'desktop_size': return 'mediu a tela';
|
|
79
|
+
case 'browser_open': return `abriu ${corta(args.url, 60)} no navegador`;
|
|
80
|
+
case 'browser_click': return `clicou em “${corta(args.text || args.selector || `${args.x},${args.y}`, 40)}” na página`;
|
|
81
|
+
case 'browser_type': return `digitou “${corta(args.text, 40)}” na página`;
|
|
82
|
+
case 'browser_key': return `tecla ${args.key} na página`;
|
|
83
|
+
case 'browser_scroll': return 'rolou a página';
|
|
84
|
+
case 'browser_read': return 'leu a página';
|
|
85
|
+
case 'browser_screenshot': return 'print da página';
|
|
86
|
+
case 'browser_wait': return `esperou ${corta(args.selector, 40)}`;
|
|
87
|
+
case 'browser_eval': return 'rodou JavaScript na página';
|
|
88
|
+
case 'write_file': return `criou/reescreveu ${corta(args.path, 50)}`;
|
|
89
|
+
case 'edit_file': return `editou ${corta(args.path, 50)}`;
|
|
90
|
+
case 'multi_edit': return `editou ${corta(args.path, 50)} (várias trocas)`;
|
|
91
|
+
case 'read_file': return `leu ${corta(args.path, 50)}`;
|
|
92
|
+
case 'run_shell': return `rodou: ${corta(args.command, 60)}`;
|
|
93
|
+
case 'run_in_new_terminal': return `terminal novo: ${corta(args.command, 50)}`;
|
|
94
|
+
case 'write_todos': return 'atualizou o plano';
|
|
95
|
+
default: return name.replace(/_/g, ' ');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
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
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Chamada pelo loop do agente depois de CADA tool. Quando o streaming está
|
|
128
|
+
* desligado é um no-op — o caminho normal não paga nada por isto.
|
|
129
|
+
*/
|
|
130
|
+
function step(name, args, result) {
|
|
131
|
+
if (!state.on) return;
|
|
132
|
+
|
|
133
|
+
const troca = nomeDeAba(name, args || {});
|
|
134
|
+
if (troca) entrarNaAba(troca);
|
|
135
|
+
if (!state.abaAtual && ACOES_NA_TELA.has(name)) entrarNaAba('Tela');
|
|
136
|
+
|
|
137
|
+
state.passo++;
|
|
138
|
+
if (state.abaAtual) state.abaAtual.passos++;
|
|
139
|
+
|
|
140
|
+
const entrada = {
|
|
141
|
+
n: state.passo,
|
|
142
|
+
hora: new Date().toTimeString().slice(0, 8),
|
|
143
|
+
acao: descrever(name, args || {}),
|
|
144
|
+
ok: !(result && result.ok === false),
|
|
145
|
+
erro: result && result.ok === false ? corta(result.error, 120) : undefined,
|
|
146
|
+
aba: state.abaAtual ? state.abaAtual.id : null,
|
|
147
|
+
};
|
|
148
|
+
|
|
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);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function capturar(entrada) {
|
|
159
|
+
const agora = Date.now();
|
|
160
|
+
if (agora - state.ultimaFoto < INTERVALO_FOTO_MS) return null;
|
|
161
|
+
state.ultimaFoto = agora;
|
|
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;
|
|
176
|
+
}
|
|
177
|
+
|
|
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
|
+
|
|
191
|
+
const PAGINA = `<!doctype html>
|
|
192
|
+
<html lang="pt-BR"><head><meta charset="utf-8">
|
|
193
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
194
|
+
<title>PrimoCode · Ao vivo</title>
|
|
195
|
+
<style>
|
|
196
|
+
* { box-sizing: border-box; margin: 0; }
|
|
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; }
|
|
206
|
+
.dot { width: 9px; height: 9px; border-radius: 50%; background: #22d3ee; animation: pulsa 1.4s infinite; }
|
|
207
|
+
@keyframes pulsa { 50% { opacity: .25 } }
|
|
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; }
|
|
235
|
+
.item .n { color: #475569; min-width: 3ch; text-align: right; }
|
|
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; }
|
|
239
|
+
.item .hora { color: #475569; font-size: 11px; }
|
|
240
|
+
.item.temfoto { cursor: pointer; } .item.temfoto:hover .txt { color: #22d3ee; }
|
|
241
|
+
|
|
242
|
+
footer { color: #475569; font-size: 12px; padding-top: 14px; }
|
|
243
|
+
</style></head>
|
|
244
|
+
<body>
|
|
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>
|
|
256
|
+
<section id="tela">
|
|
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>
|
|
260
|
+
</section>
|
|
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
|
+
|
|
268
|
+
<script>
|
|
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
|
+
|
|
335
|
+
async function tick() {
|
|
336
|
+
try {
|
|
337
|
+
const s = await (await fetch('/state')).json();
|
|
338
|
+
dados = s;
|
|
339
|
+
document.getElementById('st').textContent = 'ao vivo · porta ' + location.port;
|
|
340
|
+
const assinatura = JSON.stringify([s.frames.length, s.log.length, s.abas.length, s.erroFoto]);
|
|
341
|
+
if (assinatura !== ultimoEstado) { ultimoEstado = assinatura; render(); }
|
|
342
|
+
} catch (e) {
|
|
343
|
+
document.getElementById('st').textContent = 'reconectando…';
|
|
344
|
+
}
|
|
345
|
+
setTimeout(tick, 700);
|
|
346
|
+
}
|
|
347
|
+
tick();
|
|
348
|
+
</script>
|
|
349
|
+
</body></html>`;
|
|
350
|
+
|
|
351
|
+
function start() {
|
|
352
|
+
if (state.on) return Promise.resolve(state.url);
|
|
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 {}
|
|
356
|
+
|
|
357
|
+
return new Promise((resolve, reject) => {
|
|
358
|
+
const server = http.createServer((req, res) => {
|
|
359
|
+
const u = new URL(req.url || '/', 'http://x');
|
|
360
|
+
if (u.pathname === '/state') {
|
|
361
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
362
|
+
res.end(JSON.stringify({
|
|
363
|
+
erroFoto: state.erroFoto,
|
|
364
|
+
abas: state.abas,
|
|
365
|
+
frames: state.frames,
|
|
366
|
+
log: state.log.slice(-120),
|
|
367
|
+
}));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
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) => {
|
|
377
|
+
if (err) { res.writeHead(404); res.end(); return; }
|
|
378
|
+
// Cada frame é imutável: o navegador pode guardar à vontade.
|
|
379
|
+
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'max-age=3600' });
|
|
380
|
+
res.end(buf);
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
385
|
+
res.end(PAGINA);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
server.on('error', reject);
|
|
389
|
+
// unref: o painel não pode segurar o processo vivo — /exit tem de sair
|
|
390
|
+
// mesmo com o streaming ligado.
|
|
391
|
+
server.on('connection', (s) => s.unref());
|
|
392
|
+
// Só na própria máquina: isto é a tela do usuário, não um serviço.
|
|
393
|
+
server.listen(0, '127.0.0.1', () => {
|
|
394
|
+
server.unref();
|
|
395
|
+
state.server = server;
|
|
396
|
+
state.on = true;
|
|
397
|
+
state.url = `http://127.0.0.1:${server.address().port}`;
|
|
398
|
+
resolve(state.url);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function stop() {
|
|
404
|
+
if (state.server) { try { state.server.close(); } catch {} }
|
|
405
|
+
state.server = null;
|
|
406
|
+
state.on = false;
|
|
407
|
+
state.url = '';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
module.exports = { start, stop, step, isOn, url, descrever };
|
package/lib/tools.js
CHANGED
|
@@ -436,6 +436,22 @@ async function despachar(name, args, ctx) {
|
|
|
436
436
|
return desktop.scroll(args.dy != null ? args.dy : 300);
|
|
437
437
|
case 'desktop_size':
|
|
438
438
|
return desktop.getScreenSize();
|
|
439
|
+
case 'desktop_double_click':
|
|
440
|
+
return desktop.doubleClick(args.x, args.y);
|
|
441
|
+
case 'desktop_open_app':
|
|
442
|
+
return desktop.openApp(args.app || args.name || '');
|
|
443
|
+
case 'desktop_windows':
|
|
444
|
+
return desktop.listWindows();
|
|
445
|
+
case 'desktop_focus':
|
|
446
|
+
return desktop.focusWindow(args.window || args.title || '');
|
|
447
|
+
case 'desktop_elements':
|
|
448
|
+
return desktop.uiElements(args.window);
|
|
449
|
+
case 'desktop_click_element':
|
|
450
|
+
return desktop.clickElement(args.name || args.text || '', {
|
|
451
|
+
window: args.window,
|
|
452
|
+
double: !!args.double,
|
|
453
|
+
button: args.button,
|
|
454
|
+
});
|
|
439
455
|
|
|
440
456
|
default:
|
|
441
457
|
return { ok: false, error: `Tool não implementada: ${name}` };
|
package/lib/ui.js
CHANGED
|
@@ -161,9 +161,9 @@ const TIPS = [
|
|
|
161
161
|
];
|
|
162
162
|
|
|
163
163
|
const NEWS = [
|
|
164
|
+
'Agora ele INTERAGE com o computador: janelas, botões e ícones pelo nome',
|
|
165
|
+
'/streaming abre um painel ao vivo — veja cada passo dele na tela',
|
|
164
166
|
'Dois provedores em corrente: acabou a cota de um, o outro atende',
|
|
165
|
-
'Ele trabalha em ~/primocode. Fale o nome de uma pasta e ele muda sozinho',
|
|
166
|
-
'Falha de navegador agora diz quais seletores EXISTEM na página',
|
|
167
167
|
];
|
|
168
168
|
|
|
169
169
|
// ── Cabeçalho ────────────────────────────────────────────────────────────
|
|
@@ -309,9 +309,12 @@ function actionHeader(name, args) {
|
|
|
309
309
|
desktop_screenshot:'Desktop·shot', desktop_click:'Desktop·click', desktop_move:'Desktop·move',
|
|
310
310
|
desktop_drag:'Desktop·drag', desktop_type:'Desktop·type', desktop_key:'Desktop·key',
|
|
311
311
|
desktop_scroll:'Desktop·scroll', desktop_size:'Desktop·size',
|
|
312
|
+
desktop_double_click:'Desktop·2click', desktop_open_app:'Desktop·app',
|
|
313
|
+
desktop_windows:'Desktop·janelas', desktop_focus:'Desktop·foco',
|
|
314
|
+
desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
|
|
312
315
|
};
|
|
313
316
|
const label = nice[name] || name;
|
|
314
|
-
const argStr = args ? (args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || JSON.stringify(args).slice(0,60)) : '';
|
|
317
|
+
const argStr = args ? (args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || JSON.stringify(args).slice(0,60)) : '';
|
|
315
318
|
return ` ${c.dim('⎿')} ${c.brand(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
|
|
316
319
|
}
|
|
317
320
|
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.
|
|
4
|
-
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop.",
|
|
3
|
+
"version": "7.12.0",
|
|
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": {
|
|
7
7
|
"primocode": "bin/primocode.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|