primocode 7.12.1 → 7.13.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/act.js CHANGED
@@ -15,7 +15,10 @@ const tools = require('./tools');
15
15
  const api = require('./api');
16
16
  const cfg = require('./config');
17
17
  const catalogo = require('./catalogo');
18
- const streaming = require('./streaming');
18
+ // "painel", não "streaming": dentro do runAct existe um `let streaming` (a
19
+ // flag de texto chegando em stream) que SOMBREARIA este módulo — e o step()
20
+ // viraria TypeError engolido pelo catch. Foi um bug real, pego em teste.
21
+ const painel = require('./streaming');
19
22
 
20
23
  const MAX_ITERATIONS = 15;
21
24
  const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
@@ -182,6 +185,11 @@ async function runAct(userPrompt, opts) {
182
185
 
183
186
  let finalText = '';
184
187
 
188
+ // O pedido do usuário aparece na barra de comando do painel, digitado
189
+ // letra a letra — só o pedido principal; sub-tarefa de agente não é
190
+ // instrução de gente.
191
+ if (!subagent) { try { painel.instrucao(userPrompt); } catch {} }
192
+
185
193
  for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
186
194
  const sp = new Spinner(subagent ? 'subagente trabalhando' : 'Primo Code pensando').start();
187
195
 
@@ -282,7 +290,7 @@ async function runAct(userPrompt, opts) {
282
290
 
283
291
  // Painel ao vivo (/streaming): registra o passo e fotografa a tela.
284
292
  // Nunca pode derrubar o loop — falha de foto é problema do painel.
285
- try { streaming.step(tc.name, tc.arguments, result); } catch {}
293
+ try { painel.step(tc.name, tc.arguments, result); } catch {}
286
294
 
287
295
  // Uma escrita bem-sucedida limpa a dívida: o agente se corrigiu.
288
296
  if (result && result.ok === false) falhasPendentes.push(`${tc.name}(${tc.arguments?.path || ''}): ${result.error}`);
package/lib/desktop.js CHANGED
@@ -407,8 +407,11 @@ function listWindows() {
407
407
  return m ? `|${m[1]}` : '';
408
408
  });
409
409
  } else if (TOOLS.xdotool) {
410
+ // O xdotool sai com código ≠ 0 se UMA janela da lista não tiver nome
411
+ // — mas o stdout com as outras continua valendo. Jogar a saída fora
412
+ // por causa do código fazia o total dar 0 com janelas na cara.
410
413
  const r = runOut(`xdotool search --onlyvisible --name '.*' getwindowname %@ 2>/dev/null`);
411
- if (r.ok) linhas = r.out.split('\n').map((t) => (t.trim() ? `|${t.trim()}` : ''));
414
+ if (r.out) linhas = r.out.split('\n').map((t) => (t.trim() ? `|${t.trim()}` : ''));
412
415
  } else {
413
416
  return { ok: false, error: 'Instale wmctrl ou xdotool para listar janelas no Linux.' };
414
417
  }
@@ -464,8 +467,13 @@ Write-Output $p.MainWindowTitle`,
464
467
  const busca = runOut(`xdotool search --onlyvisible --name ${sh1(t)} 2>/dev/null`);
465
468
  const id = (busca.out || '').trim().split('\n')[0];
466
469
  if (!id) return naoAchouJanela(t);
470
+ // windowactivate depende do window manager (EWMH). Sem ele — ou com um
471
+ // WM teimoso — raise+focus fazem o mesmo serviço.
467
472
  const r = run(`xdotool windowactivate --sync ${id}`);
468
- return r.ok ? { ok: true, janela: t } : r;
473
+ if (r.ok) return { ok: true, janela: t };
474
+ const raise = run(`xdotool windowraise ${id}`);
475
+ const foco = run(`xdotool windowfocus ${id}`);
476
+ return (raise.ok || foco.ok) ? { ok: true, janela: t } : r;
469
477
  }
470
478
  return { ok: false, error: 'Focar janela não disponível: instale xdotool (Linux).' };
471
479
  }
package/lib/streaming.js CHANGED
@@ -40,6 +40,7 @@ const state = {
40
40
  abas: [], // [{ id, nome, passos: 0 }] — "Ao vivo" é virtual, não entra aqui
41
41
  abaAtual: null,
42
42
  log: [],
43
+ instrucoes: [], // o que o usuário pediu — a página autopreenche digitando
43
44
  erroFoto: null,
44
45
  ultimaFoto: 0,
45
46
  passo: 0,
@@ -123,6 +124,22 @@ function entrarNaAba(nome) {
123
124
  state.abaAtual = aba;
124
125
  }
125
126
 
127
+ /**
128
+ * A instrução que o usuário acabou de dar. A página autopreenche na barra de
129
+ * comando, letra por letra, e marca o começo dela na linha do tempo.
130
+ */
131
+ function instrucao(texto) {
132
+ if (!state.on) return;
133
+ const t = String(texto || '').trim();
134
+ if (!t) return;
135
+ state.instrucoes.push({ n: state.instrucoes.length + 1, hora: new Date().toTimeString().slice(0, 8), texto: corta(t, 300) });
136
+ if (state.instrucoes.length > 40) state.instrucoes.splice(0, state.instrucoes.length - 40);
137
+
138
+ state.passo++;
139
+ state.log.push({ n: state.passo, hora: new Date().toTimeString().slice(0, 8), tipo: 'instrucao', acao: corta(t, 160), ok: true, aba: null });
140
+ if (state.log.length > MAX_LOG) state.log.splice(0, state.log.length - MAX_LOG);
141
+ }
142
+
126
143
  /**
127
144
  * Chamada pelo loop do agente depois de CADA tool. Quando o streaming está
128
145
  * desligado é um no-op — o caminho normal não paga nada por isto.
@@ -188,67 +205,106 @@ const MASCOTE_SVG = `<svg viewBox="0 0 9 3.6" aria-hidden="true">
188
205
  <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
206
  </svg>`;
190
207
 
191
- // A página segue o desenho do dono, traço a traço: fundo grafite, mascote
192
- // inclinado no canto, "Primocode" branco no alto; a janela de borda branca
193
- // grossa e canto bem redondo; barra #232323 com as três bolinhas e as abas em
194
- // texto branco centralizado, separadas por "|"; dentro, a tela preta com a
195
- // imagem ao vivo. O histórico (miniaturas + passos) fica ABAIXO da janela,
196
- // discreto — a moldura fica idêntica ao desenho.
208
+ // A tela finalizada, a partir do desenho do dono: fundo grafite, mascote
209
+ // inclinado, "Primocode" branco no alto; a janela de borda branca grossa com
210
+ // as três bolinhas e as abas separadas por "|"; e, novidade combinada, a
211
+ // BARRA DE COMANDO — a instrução que você deu no terminal aparece ali,
212
+ // autopreenchendo letra por letra, com cursor piscando, enquanto o agente
213
+ // executa. Abaixo da moldura, a linha do tempo: miniaturas para voltar em
214
+ // qualquer passo e a lista de ações, com cada pedido marcando onde começou.
215
+ const FAVICON = 'data:image/svg+xml,' + encodeURIComponent(
216
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 3.6">${MASCOTE_SVG.replace(/^<svg[^>]*>|<\/svg>$/g, '')}</svg>`,
217
+ );
218
+
197
219
  const PAGINA = `<!doctype html>
198
220
  <html lang="pt-BR"><head><meta charset="utf-8">
199
221
  <meta name="viewport" content="width=device-width, initial-scale=1">
200
- <title>PrimoCode · Ao vivo</title>
222
+ <title>Primocode · Ao vivo</title>
223
+ <link rel="icon" href="${FAVICON}">
201
224
  <style>
202
225
  * { box-sizing: border-box; margin: 0; }
203
- body { background: #262626; color: #fff; font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 0 16px 30px; }
226
+ ::selection { background: rgba(255,255,255,.2); }
227
+ body {
228
+ background: radial-gradient(1200px 500px at 50% -100px, #2e2e2e 0%, #262626 55%, #1f1f1f 100%) fixed #262626;
229
+ color: #fff; font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
230
+ min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 0 18px 34px;
231
+ }
204
232
 
205
- /* Cabeçalho: mascote inclinado no canto, nome centrado */
206
- #topo { position: relative; width: min(1100px, 100%); padding: 44px 0 30px; text-align: center; }
207
- #topo .mascote { position: absolute; left: 6px; top: 30px; width: 96px; height: 40px; transform: rotate(-32deg); }
233
+ /* ── Cabeçalho ── */
234
+ #topo { position: relative; width: min(1120px, 100%); padding: 46px 0 28px; text-align: center; }
235
+ #topo .mascote { position: absolute; left: 4px; top: 28px; width: 100px; height: 42px; transform: rotate(-32deg); filter: drop-shadow(0 6px 14px rgba(34,211,238,.35)); animation: flutua 3.2s ease-in-out infinite alternate; }
236
+ @keyframes flutua { from { translate: 0 0 } to { translate: 0 5px } }
208
237
  #topo .mascote svg { width: 100%; height: 100%; display: block; }
209
- #topo h1 { font-size: clamp(34px, 6vw, 58px); font-weight: 600; letter-spacing: .5px; }
210
- #topo .vivo { position: absolute; right: 6px; top: 46px; display: inline-flex; align-items: center; gap: 7px; color: #8a8a8a; font-size: 12px; }
211
- .dot { width: 9px; height: 9px; border-radius: 50%; background: #46d160; animation: pulsa 1.4s infinite; }
212
- @keyframes pulsa { 50% { opacity: .25 } }
213
-
214
- /* A janela — borda branca grossa, canto bem redondo */
215
- #janela { width: min(1100px, 100%); background: #000; border: 10px solid #f5f5f5; border-radius: 46px; overflow: hidden; display: flex; flex-direction: column; }
216
- #barra { position: relative; display: flex; align-items: center; background: #232323; padding: 14px 26px; min-height: 58px; }
217
- #barra .b { flex: none; margin-right: 12px; }
218
- #barra .b.r { width: 17px; height: 17px; border-radius: 50%; background: #d40000; }
219
- #barra .b.y { width: 30px; height: 17px; border-radius: 50%; background: #ffdd00; }
220
- #barra .b.g { width: 24px; height: 14px; border-radius: 50%; background: #33cc33; }
221
-
222
- /* Abas: texto branco centralizado na barra, separadas por | */
223
- #abas { position: absolute; left: 50%; transform: translateX(-50%); max-width: 62%; display: flex; align-items: center; gap: 12px; overflow-x: auto; white-space: nowrap; scrollbar-width: none; font-size: clamp(16px, 2.4vw, 24px); font-weight: 500; }
238
+ #topo h1 { font-size: clamp(36px, 6vw, 60px); font-weight: 650; letter-spacing: .5px; text-shadow: 0 3px 22px rgba(0,0,0,.55); }
239
+ #topo .vivo { position: absolute; right: 2px; top: 50px; display: inline-flex; align-items: center; gap: 8px; color: #a9a9a9; font-size: 12.5px; background: #1c1c1cd9; border: 1px solid #363636; border-radius: 999px; padding: 6px 14px; }
240
+ .dot { width: 9px; height: 9px; border-radius: 50%; background: #46d160; box-shadow: 0 0 8px #46d16088; animation: pulsa 1.4s infinite; }
241
+ @keyframes pulsa { 50% { opacity: .3 } }
242
+
243
+ /* ── A janela ── */
244
+ #janela {
245
+ width: min(1120px, 100%); background: #000; border: 9px solid #f5f5f5; border-radius: 44px;
246
+ overflow: hidden; display: flex; flex-direction: column;
247
+ box-shadow: 0 34px 90px rgba(0,0,0,.65), 0 0 0 1px rgba(255,255,255,.06), 0 10px 34px rgba(0,0,0,.5);
248
+ }
249
+ #barra { position: relative; display: flex; align-items: center; background: linear-gradient(#262626, #212121); padding: 15px 28px; min-height: 60px; border-bottom: 1px solid #0c0c0c; }
250
+ #barra .b { flex: none; margin-right: 12px; box-shadow: inset 0 -2px 4px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.5); }
251
+ #barra .b.r { width: 17px; height: 17px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #ff5a5a, #d40000); }
252
+ #barra .b.y { width: 30px; height: 17px; border-radius: 999px; background: radial-gradient(circle at 35% 30%, #ffe95c, #f5c400); }
253
+ #barra .b.g { width: 24px; height: 14px; border-radius: 999px; background: radial-gradient(circle at 35% 30%, #6fe589, #24b53e); }
254
+
255
+ #abas { position: absolute; left: 50%; transform: translateX(-50%); max-width: 60%; display: flex; align-items: center; gap: 13px; overflow-x: auto; white-space: nowrap; scrollbar-width: none; font-size: clamp(15px, 2.2vw, 22px); font-weight: 550; }
224
256
  #abas::-webkit-scrollbar { display: none; }
225
- .aba { border: 0; background: none; cursor: pointer; font: inherit; color: rgba(255,255,255,.5); padding: 2px 2px; }
226
- .aba:hover { color: rgba(255,255,255,.8); }
227
- .aba.ativa { color: #fff; }
228
- .sep { color: rgba(255,255,255,.45); font-weight: 400; }
229
-
230
- /* A tela */
231
- #tela { position: relative; min-height: 470px; display: flex; align-items: center; justify-content: center; background: #000; }
232
- #tela img { width: 100%; max-height: 64vh; object-fit: contain; display: none; }
233
- #aviso { color: #fff; text-align: center; font-size: clamp(20px, 3.4vw, 34px); font-weight: 500; padding: 110px 20px; }
234
- #carimbo { position: absolute; left: 16px; bottom: 12px; background: rgba(0,0,0,.75); border: 1px solid #2e2e2e; color: #bdbdbd; font-size: 12px; padding: 3px 10px; border-radius: 8px; display: none; max-width: 85%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
235
-
236
- /* Histórico: fora da moldura, discreto, abaixo da janela */
237
- #historico { width: min(1100px, 100%); padding-top: 14px; }
238
- #filme { display: flex; gap: 6px; padding: 2px 2px 8px; overflow-x: auto; }
239
- .foto { flex: none; border: 1px solid #3a3a3a; border-radius: 6px; background: #000; cursor: pointer; padding: 0; width: 84px; height: 50px; overflow: hidden; opacity: .7; }
257
+ .aba { border: 0; background: none; cursor: pointer; font: inherit; color: rgba(255,255,255,.45); padding: 3px 2px; transition: color .18s ease, text-shadow .18s ease; }
258
+ .aba:hover { color: rgba(255,255,255,.85); }
259
+ .aba.ativa { color: #fff; text-shadow: 0 0 16px rgba(255,255,255,.45); }
260
+ .aba .qtd { color: rgba(255,255,255,.35); font-size: .62em; vertical-align: super; margin-left: 4px; font-weight: 600; }
261
+ .sep { color: rgba(255,255,255,.28); font-weight: 300; }
262
+
263
+ /* ── Barra de comando: a instrução autopreenchendo ── */
264
+ #comando { display: flex; align-items: center; gap: 12px; background: #141414; border-bottom: 1px solid #0a0a0a; padding: 12px 26px; font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; font-size: 14.5px; min-height: 46px; }
265
+ #comando .chevron { color: #46d160; font-weight: 700; flex: none; }
266
+ #comando .texto { color: #eaeaea; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
267
+ #comando .texto.vazio { color: #5c5c5c; font-style: italic; }
268
+ #comando .cursor { flex: none; width: 8px; height: 17px; background: #eaeaea; margin-left: 2px; animation: pisca 1s steps(1) infinite; }
269
+ @keyframes pisca { 50% { opacity: 0 } }
270
+ #comando .hora { margin-left: auto; color: #5c5c5c; font-size: 11.5px; flex: none; }
271
+
272
+ /* ── A tela ── */
273
+ #tela { position: relative; min-height: 460px; display: flex; align-items: center; justify-content: center; background: #000; }
274
+ #tela img { width: 100%; max-height: 62vh; object-fit: contain; display: none; opacity: 0; transition: opacity .28s ease; }
275
+ #aviso { color: #fff; text-align: center; padding: 90px 24px; }
276
+ #aviso .titulo { font-size: clamp(20px, 3.2vw, 32px); font-weight: 550; }
277
+ #aviso .sub { color: #8f8f8f; font-size: 14.5px; margin-top: 14px; line-height: 1.7; }
278
+ #carimbo { position: absolute; left: 18px; bottom: 14px; background: rgba(10,10,10,.72); backdrop-filter: blur(6px); border: 1px solid #2e2e2e; color: #c9c9c9; font-size: 12px; padding: 5px 13px; border-radius: 999px; display: none; max-width: 85%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
279
+ #carimbo b { color: #fff; font-weight: 600; }
280
+
281
+ /* ── Linha do tempo ── */
282
+ #historico { width: min(1120px, 100%); padding-top: 20px; }
283
+ #filme { display: flex; gap: 10px; padding: 4px 2px 14px; overflow-x: auto; scrollbar-width: thin; scrollbar-color: #3a3a3a transparent; }
284
+ .foto { flex: none; border: 1.5px solid #3a3a3a; border-radius: 10px; background: #000; cursor: pointer; padding: 0; width: 104px; height: 62px; overflow: hidden; opacity: .65; transition: transform .16s ease, opacity .16s ease, border-color .16s ease, box-shadow .16s ease; }
285
+ .foto:hover { opacity: 1; transform: translateY(-2px); border-color: #7a7a7a; }
240
286
  .foto img { width: 100%; height: 100%; object-fit: cover; display: block; }
241
- .foto.ativa { border-color: #fff; opacity: 1; }
242
- #passos { max-height: 170px; overflow-y: auto; }
243
- .item { padding: 3px 6px; display: flex; gap: 8px; font-size: 13px; }
244
- .item .n { color: #6f6f6f; min-width: 3ch; text-align: right; }
245
- .item .ok { color: #46d160; } .item .falha { color: #e5484d; }
246
- .item .txt { flex: 1; word-break: break-word; color: #d6d6d6; }
247
- .item .txt .erro { color: #e5484d; font-size: 12px; display: block; }
248
- .item .hora { color: #6f6f6f; font-size: 11px; }
287
+ .foto.ativa { border-color: #fff; opacity: 1; box-shadow: 0 0 0 2px rgba(255,255,255,.25), 0 6px 18px rgba(0,0,0,.45); }
288
+
289
+ #cartao { background: #1c1c1c; border: 1px solid #2c2c2c; border-radius: 18px; overflow: hidden; }
290
+ #cartao header { display: flex; align-items: center; justify-content: space-between; padding: 12px 18px; border-bottom: 1px solid #272727; color: #bdbdbd; font-size: 13px; font-weight: 600; letter-spacing: .3px; }
291
+ #cartao header span b { color: #fff; }
292
+ #passos { max-height: 240px; overflow-y: auto; padding: 6px 0 10px; scrollbar-width: thin; scrollbar-color: #3a3a3a transparent; }
293
+ #passos::-webkit-scrollbar { width: 8px; } #passos::-webkit-scrollbar-thumb { background: #3a3a3a; border-radius: 4px; }
294
+ .item { padding: 5px 18px; display: flex; gap: 10px; font-size: 13.5px; align-items: baseline; transition: background .12s ease; }
295
+ .item:hover { background: #222; }
296
+ .item .n { color: #6b6b6b; min-width: 3ch; text-align: right; font-variant-numeric: tabular-nums; }
297
+ .item .ok { color: #46d160; } .item .falha { color: #ff6b6b; }
298
+ .item .txt { flex: 1; word-break: break-word; color: #dcdcdc; }
299
+ .item .txt .erro { color: #ff8a8a; font-size: 12px; display: block; margin-top: 1px; }
300
+ .item .hora { color: #6b6b6b; font-size: 11px; font-variant-numeric: tabular-nums; }
249
301
  .item.temfoto { cursor: pointer; } .item.temfoto:hover .txt { color: #fff; }
302
+ .item.pedido { background: #191919; border-top: 1px solid #262626; border-bottom: 1px solid #262626; margin: 6px 0; padding: 8px 18px; }
303
+ .item.pedido .txt { color: #fff; font-weight: 600; }
304
+ .item.pedido .chev { color: #46d160; font-weight: 800; }
250
305
 
251
- footer { color: #6f6f6f; font-size: 12px; padding-top: 14px; }
306
+ footer { color: #6b6b6b; font-size: 12px; padding-top: 18px; }
307
+ footer b { color: #9a9a9a; font-weight: 600; }
252
308
  </style></head>
253
309
  <body>
254
310
  <div id="topo">
@@ -262,43 +318,76 @@ const PAGINA = `<!doctype html>
262
318
  <span class="b r"></span><span class="b y"></span><span class="b g"></span>
263
319
  <nav id="abas"></nav>
264
320
  </div>
321
+ <div id="comando">
322
+ <span class="chevron">❯</span>
323
+ <span class="texto vazio" id="cmdtexto">aguardando a sua instrução no terminal…</span>
324
+ <span class="cursor"></span>
325
+ <span class="hora" id="cmdhora"></span>
326
+ </div>
265
327
  <section id="tela">
266
328
  <img id="frame" alt="tela que a IA está usando">
267
- <div id="aviso">Tela que a IA está</div>
329
+ <div id="aviso">
330
+ <div class="titulo">Tela que a IA está usando aparece aqui</div>
331
+ <div class="sub">Dê uma instrução no terminal do PrimoCode e veja cada passo, ao vivo.<br>Cada app em que ele mexer vira uma aba — volte nelas para rever o que já foi feito.</div>
332
+ </div>
268
333
  <div id="carimbo"></div>
269
334
  </section>
270
335
  </div>
271
336
 
272
337
  <div id="historico">
273
338
  <div id="filme"></div>
274
- <div id="passos"></div>
339
+ <div id="cartao">
340
+ <header><span>Linha do tempo · <b id="abanome">Ao vivo</b></span><span id="resumo"></span></header>
341
+ <div id="passos"></div>
342
+ </div>
275
343
  </div>
276
- <footer>só nesta máquina (127.0.0.1) · /streaming desliga</footer>
344
+ <footer>só nesta máquina <b>(127.0.0.1)</b> · <b>/streaming</b> desliga</footer>
277
345
 
278
346
  <script>
279
347
  const VIVO = 0; // id da aba "Ao vivo"
280
- let dados = { abas: [], frames: [], log: [] };
348
+ let dados = { abas: [], frames: [], log: [], instrucoes: [] };
281
349
  let abaSel = VIVO; // aba escolhida pelo usuário
282
350
  let fotoSel = null; // frame fixado no filmstrip (null = mais recente)
283
351
  let ultimoEstado = '';
284
352
 
353
+ // ── autopreencher a instrução, letra por letra ──
354
+ let digitando = null, instrucaoNaTela = 0;
355
+ function autopreencher(inst) {
356
+ if (!inst || inst.n === instrucaoNaTela) return;
357
+ instrucaoNaTela = inst.n;
358
+ clearInterval(digitando);
359
+ const el = document.getElementById('cmdtexto');
360
+ el.classList.remove('vazio');
361
+ el.textContent = '';
362
+ document.getElementById('cmdhora').textContent = inst.hora;
363
+ let i = 0;
364
+ digitando = setInterval(() => {
365
+ i += 1 + Math.floor(Math.random() * 2); // cadência de gente digitando
366
+ el.textContent = inst.texto.slice(0, i);
367
+ if (i >= inst.texto.length) clearInterval(digitando);
368
+ }, 34);
369
+ }
370
+
285
371
  function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
286
372
 
287
373
  function framesDaAba(id) {
288
374
  return id === VIVO ? dados.frames : dados.frames.filter((f) => f.aba === id);
289
375
  }
290
376
  function logDaAba(id) {
291
- return id === VIVO ? dados.log : dados.log.filter((e) => e.aba === id);
377
+ return id === VIVO ? dados.log : dados.log.filter((e) => e.aba === id || e.tipo === 'instrucao');
292
378
  }
293
379
 
294
380
  function render() {
295
- // Abas: "Ao vivo" + uma por app/janela, texto separado por |
381
+ // Abas: "Ao vivo" + uma por app/janela, separadas por |
296
382
  const nav = document.getElementById('abas');
297
383
  const abas = [{ id: VIVO, nome: 'Ao vivo' }, ...dados.abas];
298
384
  nav.innerHTML = abas.map((a) =>
299
- '<button class="aba' + (a.id === abaSel ? ' ativa' : '') + '" data-id="' + a.id + '">' + esc(a.nome) + '</button>'
385
+ '<button class="aba' + (a.id === abaSel ? ' ativa' : '') + '" data-id="' + a.id + '">' + esc(a.nome)
386
+ + (a.passos ? '<span class="qtd">' + a.passos + '</span>' : '') + '</button>'
300
387
  ).join('<span class="sep">|</span>');
301
388
  nav.querySelectorAll('.aba').forEach((b) => b.onclick = () => { abaSel = Number(b.dataset.id); fotoSel = null; render(); });
389
+ const abaObj = abas.find((a) => a.id === abaSel) || abas[0];
390
+ document.getElementById('abanome').textContent = abaObj.nome;
302
391
 
303
392
  // Imagem: a fixada no filmstrip, senão a mais nova da aba
304
393
  const fs = framesDaAba(abaSel);
@@ -307,19 +396,23 @@ function render() {
307
396
  const aviso = document.getElementById('aviso');
308
397
  const carimbo = document.getElementById('carimbo');
309
398
  if (atual) {
310
- const src = '/frame?n=' + atual.id;
311
- if (img.dataset.n !== String(atual.id)) { img.src = src; img.dataset.n = String(atual.id); }
399
+ if (img.dataset.n !== String(atual.id)) {
400
+ img.style.opacity = '0';
401
+ img.onload = () => { img.style.opacity = '1'; };
402
+ img.src = '/frame?n=' + atual.id;
403
+ img.dataset.n = String(atual.id);
404
+ }
312
405
  img.style.display = 'block'; aviso.style.display = 'none';
313
406
  carimbo.style.display = 'block';
314
- carimbo.textContent = '#' + atual.id + ' · ' + atual.hora + ' · ' + atual.acao + (fotoSel != null ? ' (clique na miniatura de novo para voltar ao mais recente)' : '');
407
+ carimbo.innerHTML = '<b>#' + atual.id + '</b> · ' + atual.hora + ' · ' + esc(atual.acao) + (fotoSel != null ? ' — <b>clique na miniatura de novo para voltar ao ao-vivo</b>' : '');
315
408
  } else {
316
409
  img.style.display = 'none'; aviso.style.display = 'block'; carimbo.style.display = 'none';
317
- if (dados.erroFoto) aviso.textContent = 'Sem foto da tela: ' + dados.erroFoto;
410
+ if (dados.erroFoto) aviso.querySelector('.sub').textContent = 'Sem foto da tela: ' + dados.erroFoto;
318
411
  }
319
412
 
320
- // Filmstrip da aba (as últimas 14)
413
+ // Filmstrip da aba (as últimas 16)
321
414
  const filme = document.getElementById('filme');
322
- const tira = fs.slice(-14);
415
+ const tira = fs.slice(-16);
323
416
  filme.innerHTML = tira.map((f) =>
324
417
  '<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('');
325
418
  filme.querySelectorAll('.foto').forEach((b) => b.onclick = () => {
@@ -329,14 +422,20 @@ function render() {
329
422
  });
330
423
  if (fotoSel == null) filme.scrollLeft = filme.scrollWidth;
331
424
 
332
- // Passos da aba (mais novos primeiro)
333
- const passos = logDaAba(abaSel).slice(-40).reverse();
334
- document.getElementById('passos').innerHTML = passos.map((e) =>
335
- '<div class="item' + (e.frame ? ' temfoto" data-frame="' + e.frame : '') + '">'
336
- + '<span class="n">' + e.n + '</span>'
337
- + '<span class="' + (e.ok ? 'ok' : 'falha') + '">' + (e.ok ? '✔' : '✗') + '</span>'
338
- + '<span class="txt">' + esc(e.acao) + (e.erro ? '<span class="erro">' + esc(e.erro) + '</span>' : '') + '</span>'
339
- + '<span class="hora">' + e.hora + '</span></div>').join('');
425
+ // Linha do tempo da aba (mais novos primeiro); pedidos viram divisórias
426
+ const passos = logDaAba(abaSel).slice(-60).reverse();
427
+ document.getElementById('resumo').textContent = passos.filter((e) => e.tipo !== 'instrucao').length + ' passos';
428
+ document.getElementById('passos').innerHTML = passos.map((e) => {
429
+ if (e.tipo === 'instrucao') {
430
+ return '<div class="item pedido"><span class="n">' + e.n + '</span><span class="chev">❯</span>'
431
+ + '<span class="txt">' + esc(e.acao) + '</span><span class="hora">' + e.hora + '</span></div>';
432
+ }
433
+ return '<div class="item' + (e.frame ? ' temfoto" data-frame="' + e.frame : '') + '">'
434
+ + '<span class="n">' + e.n + '</span>'
435
+ + '<span class="' + (e.ok ? 'ok' : 'falha') + '">' + (e.ok ? '✔' : '✗') + '</span>'
436
+ + '<span class="txt">' + esc(e.acao) + (e.erro ? '<span class="erro">' + esc(e.erro) + '</span>' : '') + '</span>'
437
+ + '<span class="hora">' + e.hora + '</span></div>';
438
+ }).join('');
340
439
  document.querySelectorAll('.item.temfoto').forEach((el) => el.onclick = () => { fotoSel = Number(el.dataset.frame); render(); });
341
440
  }
342
441
 
@@ -345,6 +444,7 @@ async function tick() {
345
444
  const s = await (await fetch('/state')).json();
346
445
  dados = s;
347
446
  document.getElementById('st').textContent = 'ao vivo';
447
+ autopreencher(s.instrucoes[s.instrucoes.length - 1]);
348
448
  const assinatura = JSON.stringify([s.frames.length, s.log.length, s.abas.length, s.erroFoto]);
349
449
  if (assinatura !== ultimoEstado) { ultimoEstado = assinatura; render(); }
350
450
  } catch (e) {
@@ -373,6 +473,7 @@ function start() {
373
473
  abas: state.abas,
374
474
  frames: state.frames,
375
475
  log: state.log.slice(-120),
476
+ instrucoes: state.instrucoes.slice(-20),
376
477
  }));
377
478
  return;
378
479
  }
@@ -416,4 +517,4 @@ function stop() {
416
517
  state.url = '';
417
518
  }
418
519
 
419
- module.exports = { start, stop, step, isOn, url, descrever };
520
+ module.exports = { start, stop, step, instrucao, isOn, url, descrever };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "7.12.1",
3
+ "version": "7.13.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": {