@luanpdd/kit-mcp 1.2.0 → 1.2.3

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/CHANGELOG.md CHANGED
@@ -6,6 +6,86 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.2.3] - 2026-05-04
10
+
11
+ UI inteira agora fala português e usa termos que fazem sentido pra quem não conhece o código por dentro. Os tipos de evento técnicos viraram nomes legíveis, os caminhos absolutos viraram descrições do tipo "agente planner" / "comando novo-marco" / "skill limpeza", e o status badge da conexão agora lê "CONECTADO/CONECTANDO/DESCONECTADO".
12
+
13
+ ### Adicionado — humanização de labels
14
+
15
+ - `EVENT_TYPE_LABEL` mapeia `run.start → Iniciado`, `run.end → Finalizado`, `progress → Em andamento`, `tool_invocation → Comando`, `milestone → Marco`, `error → Erro`, `shutdown → Desligado`. O `data-type` raw continua na markup (CSS de cor + filtros funcionam igual), apenas o texto exibido muda.
16
+ - `TOOL_LABEL` mapeia ids técnicos pra descrições amigáveis: `sync.install → Sincronizando kit`, `reverse-sync.apply → Importando edições do IDE`, `gates.run → Executando gate`, `sync.watch → Vigiando kit (watch)`, `sidecar → Servidor sidecar`.
17
+ - `STATUS_LABEL` traduz `running → em execução`, `done → concluído`, `error → erro`. Usado nos badges dos cards de active runs e no label de fade-out.
18
+ - `CONN_LABEL` traduz `CONNECTING → CONECTANDO`, `OPEN → CONECTADO`, `CLOSED → DESCONECTADO`.
19
+ - `humanizePath()` reconhece padrões comuns e devolve descrições amigáveis:
20
+ - `.claude/agents/planner.md` → `agente planner`
21
+ - `kit/commands/novo-marco.md` → `comando novo-marco`
22
+ - `kit/skills/limpeza/SKILL.md` → `skill limpeza`
23
+ - `.claude/framework/...` → `framework`
24
+ - `CLAUDE.md` → `manifesto CLAUDE.md`
25
+ - `humanizeLabel()` reconhece o padrão "verbo + caminho" comum em payloads de progresso e traduz: `writing .claude/agents/planner.md` → `criando agente planner`, `merging kit/commands/foo.md` → `mesclando comando foo`. Verbos suportados: reading, writing, projecting, merging, copying, deleting, creating, updating, syncing, applying, fetching.
26
+
27
+ ### Adicionado — copy PT-BR
28
+
29
+ Toda a interface está em português:
30
+ - Placeholder do search: `filtrar por nome ou conteúdo…`
31
+ - Botões: `⏸ pausar` / `▶ retomar`, `↧ rolagem auto`, `limpar tela`
32
+ - Header active runs: `Em execução agora`
33
+ - Footer: `eventos: N`, `pausado: N em fila`, `fonte: ao vivo`
34
+ - Header port: `porta NNNN`
35
+ - Estados de timing: `há 2m 15s` (substitui `started 2m 15s`)
36
+ - Status do card: `em execução` / `concluído` / `erro`
37
+ - Fim de run: `concluído com sucesso` / `falhou` (em vez de `done` / `failed`)
38
+ - Início de run: `iniciando…` (em vez de `starting…`)
39
+
40
+ ### Por que
41
+
42
+ A v1.2.2 tinha layout bonito mas linguagem técnica — `RUN.START`, `writing .claude/agents/example-reviewer.md`, `RUNNING`. Pra quem usa o sidecar pra primeira vez ou não conhece a arquitetura interna do kit, esses labels eram opacos. Agora o painel diz "Sincronizando kit · em execução · criando agente example-reviewer · 34/100 · 12s" em vez de jogar paths absolutos e enums no usuário.
43
+
44
+ ### Sem mudanças de API
45
+
46
+ Pure UI patch, ainda. Stable API v1.0+ preservada. Sem deps novas. `data-type` no DOM continua raw (filtros e CSS não quebram). Apenas `src/ui/static/index.html` mudou.
47
+
48
+ ## [1.2.2] - 2026-05-04
49
+
50
+ UX upgrade da sidecar: o feed cronológico continua, mas agora a janela mostra TAMBÉM um painel "Active runs" no topo com cards visuais pra cada execução em andamento — barra de progresso animada, percentual grande, label do passo atual, runId truncado e tempo decorrido tickando ao vivo.
51
+
52
+ ### Adicionado
53
+
54
+ - **Active runs panel** em `src/ui/static/index.html` — uma `<section id="active-runs">` antes do log de eventos. Para cada `runId` ativo, renderiza um card com:
55
+ - Nome do tool (de `payload.tool` no `run.start`) com badge de status (running/done/error)
56
+ - Percentual grande (22px tabular-nums) à direita
57
+ - Barra de progresso 8px com transição suave + shimmer animado em estado `running`
58
+ - Label do passo atual (último `payload.label` ou `current/total` derivado)
59
+ - Footer com tempo decorrido (tick a cada 1s) + runId truncado + current/total
60
+ - Estado consolidado via `Map<runId, ActiveRun>`, atualizado por `run.start` (cria), `progress` (incrementa), `tool_invocation` (refina título), `run.end` (marca done, fade-out em 4s), `error` (marca erro, fade-out em 8s).
61
+ - Cards são **atualizados in-place** via `dataset.runid` matching — preserva a transição CSS da barra em vez de recriar o DOM a cada update.
62
+ - Painel some quando 0 runs ativos; aparece automaticamente quando o primeiro `run.start` chega.
63
+
64
+ ### Por que
65
+
66
+ A v1.2.0 mostrava progresso, mas misturado no feed cronológico — pra saber "tô em quanto" você precisava scanar o último `progress`. O painel novo mostra o estado atual sem rolagem, com afordância visual: barra cresce, percentual sobe, shimmer anda. Quando termina, o card vira verde e some 4s depois (vermelho 8s pra erro). Pareceu "log do servidor", agora parece "process viewer".
67
+
68
+ ### Sem mudanças de API
69
+
70
+ Pure UI patch. Stable API v1.0+ preservada. Sem deps novas. Apenas `src/ui/static/index.html` (~120 LOC adicionados — CSS dos cards + lógica do `upsertActiveRun` + tick interval).
71
+
72
+ ## [1.2.1] - 2026-05-04
73
+
74
+ Cosmetic + UX patches descobertos durante o smoke da v1.2.0. Sem mudanças de comportamento de API.
75
+
76
+ ### Corrigido
77
+
78
+ - **`eventLabel()` agora lê `payload.name`.** Eventos `milestone` que usavam `payload.name` (sem `label`) renderizavam como texto cru "milestone" em vez do nome real. Adicionado fallback `name` na cadeia de helpers no `src/ui/static/index.html`.
79
+ - **SSE reconecta quando o tab volta a ficar visível.** Chrome (e outros browsers Chromium) throttla timers em background tabs, podendo suspender o retry interno do `EventSource` e deixar a conexão presa em `CLOSED` mesmo depois do `kit ui` voltar. Adicionado listener `visibilitychange` que faz `hydrateFromState() → connect()` quando o tab volta a `visible` e o status atual é `CLOSED`. Re-hidrata o ring buffer pra mostrar eventos que chegaram durante o gap.
80
+
81
+ ### Sem mudanças de API
82
+
83
+ `v1.2.0 → v1.2.1` é puro patch:
84
+ - Stable API v1.0+ preservada
85
+ - Sem deps novas (deps em 6/6)
86
+ - Sem mudança em `src/core/`, `src/cli/`, `src/mcp-server/`, ou em qualquer schema MCP/CLI
87
+ - Apenas `src/ui/static/index.html` recebeu ~10 LOC
88
+
9
89
  ## [1.2.0] - 2026-05-04
10
90
 
11
91
  **GUI sidecar de acompanhamento.** Janela web localhost paralela mostra ao vivo (via Server-Sent Events) o que kit-mcp está fazendo enquanto sua IDE chama tools — `sync install`, `reverse-sync apply`, `gates run`. Sidecar é totalmente opt-in: quem não invoca `kit ui` continua com a experiência v1.1 idêntica.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luanpdd/kit-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.2.3",
4
4
  "description": "Generic infrastructure to ship YOUR personal kit of agents/commands/skills as an MCP server, with cross-IDE sync (Claude Code, Cursor, Codex, Gemini, Windsurf, Antigravity, Copilot, Trae).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -275,6 +275,147 @@
275
275
  max-height: 280px;
276
276
  }
277
277
 
278
+ /* Active runs panel — current executions with live progress bars */
279
+ .runs {
280
+ margin: 12px 0 16px;
281
+ display: grid;
282
+ gap: 10px;
283
+ }
284
+ .runs[hidden] { display: none; }
285
+ .runs-header {
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 8px;
289
+ color: var(--fg-subtle);
290
+ font-family: var(--mono);
291
+ font-size: 11px;
292
+ text-transform: uppercase;
293
+ letter-spacing: .8px;
294
+ }
295
+ .runs-header .count {
296
+ background: var(--accent);
297
+ color: white;
298
+ padding: 1px 8px;
299
+ border-radius: 999px;
300
+ font-weight: 700;
301
+ }
302
+
303
+ .run-card {
304
+ background: var(--bg-elev);
305
+ border: 1px solid var(--border);
306
+ border-radius: 10px;
307
+ padding: 14px 16px;
308
+ box-shadow: var(--shadow);
309
+ display: grid;
310
+ grid-template-columns: minmax(0, 1fr) auto;
311
+ gap: 10px 16px;
312
+ align-items: baseline;
313
+ position: relative;
314
+ overflow: hidden;
315
+ transition: opacity .25s ease;
316
+ min-width: 0;
317
+ }
318
+ .run-card > * { min-width: 0; }
319
+ .run-card[data-status="running"] {
320
+ border-color: var(--accent);
321
+ border-left-width: 4px;
322
+ }
323
+ .run-card[data-status="done"] {
324
+ border-color: var(--ok);
325
+ border-left-width: 4px;
326
+ }
327
+ .run-card[data-status="error"] {
328
+ border-color: var(--err);
329
+ border-left-width: 4px;
330
+ background: color-mix(in srgb, var(--err) 5%, var(--bg-elev));
331
+ }
332
+ .run-card.fading { opacity: .4; }
333
+
334
+ .run-card .run-title {
335
+ font-family: var(--sans);
336
+ font-weight: 600;
337
+ font-size: 14px;
338
+ color: var(--fg);
339
+ display: flex;
340
+ align-items: center;
341
+ gap: 8px;
342
+ }
343
+ .run-card .run-kind {
344
+ font-size: 11px;
345
+ font-family: var(--mono);
346
+ color: var(--fg-subtle);
347
+ text-transform: uppercase;
348
+ letter-spacing: .5px;
349
+ background: var(--bg-row);
350
+ padding: 1px 8px;
351
+ border-radius: 999px;
352
+ border: 1px solid var(--border);
353
+ }
354
+ .run-card .run-percent {
355
+ font-family: var(--mono);
356
+ font-size: 22px;
357
+ font-weight: 700;
358
+ font-variant-numeric: tabular-nums;
359
+ color: var(--accent);
360
+ line-height: 1;
361
+ }
362
+ .run-card[data-status="done"] .run-percent { color: var(--ok); }
363
+ .run-card[data-status="error"] .run-percent { color: var(--err); font-size: 13px; }
364
+
365
+ .run-card .run-bar-wrap {
366
+ grid-column: 1 / -1;
367
+ height: 8px;
368
+ background: var(--bg-row);
369
+ border-radius: 4px;
370
+ overflow: hidden;
371
+ border: 1px solid var(--border);
372
+ position: relative;
373
+ }
374
+ .run-card .run-bar {
375
+ height: 100%;
376
+ background: var(--accent);
377
+ border-radius: 3px;
378
+ transition: width .25s ease;
379
+ position: relative;
380
+ }
381
+ .run-card[data-status="done"] .run-bar { background: var(--ok); }
382
+ .run-card[data-status="error"] .run-bar { background: var(--err); }
383
+ .run-card[data-status="running"] .run-bar::after {
384
+ /* Subtle moving stripe so the bar feels alive even when % stays put briefly */
385
+ content: '';
386
+ position: absolute; inset: 0;
387
+ background-image: linear-gradient(
388
+ 90deg,
389
+ transparent 0%, transparent 40%,
390
+ color-mix(in srgb, white 25%, transparent) 50%,
391
+ transparent 60%, transparent 100%
392
+ );
393
+ background-size: 200% 100%;
394
+ animation: shimmer 1.6s linear infinite;
395
+ }
396
+ @keyframes shimmer {
397
+ 0% { background-position: 200% 0; }
398
+ 100% { background-position: -200% 0; }
399
+ }
400
+
401
+ .run-card .run-step {
402
+ grid-column: 1 / -1;
403
+ color: var(--fg-muted);
404
+ font-family: var(--mono);
405
+ font-size: 12px;
406
+ word-break: break-word;
407
+ }
408
+ .run-card .run-meta {
409
+ grid-column: 1 / -1;
410
+ display: flex;
411
+ gap: 12px;
412
+ color: var(--fg-subtle);
413
+ font-family: var(--mono);
414
+ font-size: 10px;
415
+ }
416
+ .run-card .run-meta .runid { letter-spacing: .5px; }
417
+ .run-card[data-status="error"] .run-step { color: var(--err); }
418
+
278
419
  .banner {
279
420
  margin: 12px 0 0;
280
421
  padding: 12px 16px;
@@ -309,37 +450,46 @@
309
450
  <body>
310
451
  <header>
311
452
  <h1>kit-mcp sidecar</h1>
312
- <span class="meta" id="meta-port">port —</span>
453
+ <span class="meta" id="meta-port">porta —</span>
313
454
  <span class="grow"></span>
314
- <span class="conn-status" id="conn" data-state="CONNECTING"><span class="dot"></span><span id="conn-text">CONNECTING</span></span>
455
+ <span class="conn-status" id="conn" data-state="CONNECTING"><span class="dot"></span><span id="conn-text">CONECTANDO</span></span>
315
456
  </header>
316
457
 
317
458
  <div class="toolbar">
318
- <input type="search" id="search" placeholder="filter by label or payload…" autocomplete="off">
319
- <fieldset class="filters" id="type-filters" aria-label="Event types">
459
+ <input type="search" id="search" placeholder="filtrar por nome ou conteúdo…" autocomplete="off">
460
+ <fieldset class="filters" id="type-filters" aria-label="Tipos de evento">
320
461
  <!-- populated from EVENT_TYPES at runtime -->
321
462
  </fieldset>
322
- <button id="pause-btn" aria-pressed="false">⏸ pause</button>
323
- <button id="autoscroll-btn" aria-pressed="true">↧ autoscroll</button>
324
- <button id="clear-btn" title="Clear visible events (ring buffer untouched)">clear view</button>
463
+ <button id="pause-btn" aria-pressed="false">⏸ pausar</button>
464
+ <button id="autoscroll-btn" aria-pressed="true">↧ rolagem auto</button>
465
+ <button id="clear-btn" title="Limpa o que está visível (histórico no servidor preservado)">limpar tela</button>
325
466
  </div>
326
467
 
327
468
  <main>
328
469
  <div class="banner" id="shutdown-banner" hidden>
329
470
  <strong>Sidecar encerrou.</strong> Recarregue depois de <code>kit ui start</code>.
330
471
  </div>
472
+
473
+ <section class="runs" id="active-runs" hidden aria-label="Em execução agora">
474
+ <div class="runs-header">
475
+ <span>Em execução agora</span>
476
+ <span class="count" id="active-runs-count">0</span>
477
+ </div>
478
+ <div id="active-runs-list"></div>
479
+ </section>
480
+
331
481
  <ul class="ev-list" id="events" hidden></ul>
332
482
  <div class="empty" id="empty">
333
483
  <strong>Aguardando primeiro evento…</strong>
334
484
  Rode <code>kit sync install</code>, <code>kit reverse-sync apply</code>, ou
335
- invoke uma tool MCP com <code>autoSpawn: true</code> em outra janela.
485
+ chame uma ferramenta MCP com <code>autoSpawn: true</code> em outra janela.
336
486
  </div>
337
487
  </main>
338
488
 
339
489
  <div class="footer">
340
- <span id="footer-events">events: 0</span>
341
- <span id="footer-paused" hidden>paused: 0 buffered</span>
342
- <span id="footer-source">source: live</span>
490
+ <span id="footer-events">eventos: 0</span>
491
+ <span id="footer-paused" hidden>pausado: 0 em fila</span>
492
+ <span id="footer-source">fonte: ao vivo</span>
343
493
  </div>
344
494
 
345
495
  <script>
@@ -352,6 +502,105 @@
352
502
  const EVENT_TYPES = ['run.start', 'run.end', 'tool_invocation', 'progress', 'milestone', 'error', 'shutdown'];
353
503
  const RING_DISPLAY_MAX = 500;
354
504
 
505
+ // ------- Humanization (PT-BR friendly labels) ------------------------
506
+ // Maps raw technical event types and tool ids to short human-readable
507
+ // labels. The technical values stay in the data attributes (so CSS colors
508
+ // and filters still key off the raw type) — only the display text changes.
509
+
510
+ const EVENT_TYPE_LABEL = {
511
+ 'run.start': 'Iniciado',
512
+ 'run.end': 'Finalizado',
513
+ 'tool_invocation': 'Comando',
514
+ 'progress': 'Em andamento',
515
+ 'milestone': 'Marco',
516
+ 'error': 'Erro',
517
+ 'shutdown': 'Desligado',
518
+ };
519
+
520
+ const TOOL_LABEL = {
521
+ 'sync.install': 'Sincronizando kit',
522
+ 'sync.watch': 'Vigiando kit (watch)',
523
+ 'reverse-sync.apply': 'Importando edições do IDE',
524
+ 'gates.run': 'Executando gate',
525
+ 'sidecar': 'Servidor sidecar',
526
+ };
527
+
528
+ const STATUS_LABEL = {
529
+ running: 'em execução',
530
+ done: 'concluído',
531
+ error: 'erro',
532
+ };
533
+
534
+ const CONN_LABEL = {
535
+ CONNECTING: 'CONECTANDO',
536
+ OPEN: 'CONECTADO',
537
+ CLOSED: 'DESCONECTADO',
538
+ };
539
+
540
+ const PATH_VERB = {
541
+ reading: 'lendo',
542
+ writing: 'criando',
543
+ projecting: 'projetando',
544
+ merging: 'mesclando',
545
+ copying: 'copiando',
546
+ deleting: 'removendo',
547
+ creating: 'criando',
548
+ updating: 'atualizando',
549
+ syncing: 'sincronizando',
550
+ 'sync': 'sincronizando',
551
+ applying: 'aplicando',
552
+ fetching: 'buscando',
553
+ };
554
+
555
+ function humanizeEventType(t) { return EVENT_TYPE_LABEL[t] || t; }
556
+ function humanizeTool(t) { return TOOL_LABEL[t] || t; }
557
+ function humanizeStatus(s) { return STATUS_LABEL[s] || s; }
558
+
559
+ // Turn a path / file reference inside a label into a friendlier description.
560
+ // Examples:
561
+ // .claude/agents/planner.md → agente planner
562
+ // kit/commands/novo-marco.md → comando novo-marco
563
+ // kit/skills/limpeza/SKILL.md → skill limpeza
564
+ // .claude/framework/templates/codebase/x.md → template framework
565
+ // CLAUDE.md → manifesto CLAUDE.md
566
+ function humanizePath(p) {
567
+ if (typeof p !== 'string' || !p.length) return '';
568
+ const norm = p.replace(/\\/g, '/');
569
+
570
+ let m;
571
+ if ((m = norm.match(/(?:\.claude|kit)\/agents\/([^/]+)\.md$/))) return `agente ${m[1]}`;
572
+ if ((m = norm.match(/(?:\.claude|kit)\/commands\/([^/]+)\.md$/))) return `comando ${m[1]}`;
573
+ if ((m = norm.match(/(?:\.claude|kit)\/skills\/([^/]+)/))) return `skill ${m[1]}`;
574
+ if ((m = norm.match(/(?:\.claude|kit)\/framework\//))) return 'framework';
575
+ if ((m = norm.match(/(?:\.claude|kit)\/hooks\//))) return 'hooks';
576
+ if (norm === 'CLAUDE.md' || norm.endsWith('/CLAUDE.md')) return 'manifesto CLAUDE.md';
577
+ if ((m = norm.match(/(?:\.claude|kit)\/([^/]+)\/([^/]+\.md)$/))) return `${m[1]} ${m[2].replace(/\.md$/, '')}`;
578
+ return norm; // fall back to the raw path if no rule matched
579
+ }
580
+
581
+ // Translate a raw progress label like "writing .claude/agents/planner.md"
582
+ // into "criando agente planner". The function is best-effort: if no rule
583
+ // matches, returns the original label so we never lose information.
584
+ function humanizeLabel(raw) {
585
+ if (typeof raw !== 'string' || !raw.length) return raw;
586
+
587
+ // verb + path (single token after verb)
588
+ const verbMatch = raw.match(/^(reading|writing|projecting|merging|copying|deleting|creating|updating|syncing|sync|applying|fetching)\s+(.+?)(?:\s+\(.+\))?$/i);
589
+ if (verbMatch) {
590
+ const verb = PATH_VERB[verbMatch[1].toLowerCase()] || verbMatch[1];
591
+ const target = humanizePath(verbMatch[2].trim());
592
+ return `${verb} ${target}`;
593
+ }
594
+
595
+ // bare path
596
+ if (/^[^\s]+\.md$/.test(raw) || /^(?:\.claude|kit)\//.test(raw)) {
597
+ return humanizePath(raw);
598
+ }
599
+
600
+ // pure technical action, no path: leave as-is
601
+ return raw;
602
+ }
603
+
355
604
  const $ = (id) => document.getElementById(id);
356
605
  const dom = {
357
606
  conn: $('conn'),
@@ -368,6 +617,9 @@
368
617
  footerEvents: $('footer-events'),
369
618
  footerPaused: $('footer-paused'),
370
619
  footerSource: $('footer-source'),
620
+ activeRuns: $('active-runs'),
621
+ activeRunsList: $('active-runs-list'),
622
+ activeRunsCount: $('active-runs-count'),
371
623
  };
372
624
 
373
625
  const state = {
@@ -404,15 +656,17 @@
404
656
  }
405
657
 
406
658
  function eventLabel(evt) {
407
- // Best-effort short label from payload; fallback to type.
659
+ // Best-effort short label from payload; humanizes paths and tool ids;
660
+ // falls back to the humanized event type when payload has nothing useful.
408
661
  const p = evt.payload;
409
- if (!p || typeof p !== 'object') return evt.type;
410
- if (typeof p.label === 'string') return p.label;
411
- if (typeof p.tool === 'string') return `tool: ${p.tool}`;
662
+ if (!p || typeof p !== 'object') return humanizeEventType(evt.type);
663
+ if (typeof p.label === 'string') return humanizeLabel(p.label);
664
+ if (typeof p.name === 'string') return p.name;
665
+ if (typeof p.tool === 'string') return humanizeTool(p.tool);
412
666
  if (typeof p.percent === 'number') return `${p.percent}%${p.kind ? ' · ' + p.kind : ''}`;
413
667
  if (typeof p.message === 'string') return p.message;
414
668
  if (typeof p.reason === 'string') return p.reason;
415
- return evt.type;
669
+ return humanizeEventType(evt.type);
416
670
  }
417
671
 
418
672
  function eventMeta(evt) {
@@ -426,7 +680,8 @@
426
680
 
427
681
  function renderEventRow(evt) {
428
682
  const time = el('div', { class: 'ev-time', text: fmtTime(evt.ts) });
429
- const badge = el('span', { class: 'ev-badge', data: { type: evt.type }, text: evt.type });
683
+ // data-type stays raw so CSS color rules continue to apply; display text is humanized.
684
+ const badge = el('span', { class: 'ev-badge', data: { type: evt.type }, text: humanizeEventType(evt.type) });
430
685
  const summary = el('summary', {}, [
431
686
  el('span', { class: 'label', text: eventLabel(evt) }),
432
687
  el('span', { class: 'meta', text: eventMeta(evt) }),
@@ -465,7 +720,7 @@
465
720
  dom.empty.hidden = true;
466
721
  dom.list.hidden = false;
467
722
  }
468
- dom.footerEvents.textContent = `events: ${state.events.length}` + (visible !== state.events.length ? ` (showing ${visible})` : '');
723
+ dom.footerEvents.textContent = `eventos: ${state.events.length}` + (visible !== state.events.length ? ` (mostrando ${visible})` : '');
469
724
  }
470
725
 
471
726
  function pushVisibleEvent(evt) {
@@ -482,10 +737,14 @@
482
737
  }
483
738
 
484
739
  function ingestEvent(evt) {
740
+ // Always update the active-runs panel — that view is the live "what's
741
+ // happening NOW" and shouldn't be affected by the pause toggle below.
742
+ upsertActiveRun(evt);
743
+
485
744
  if (state.paused) {
486
745
  state.pausedBuffer.push(evt);
487
746
  dom.footerPaused.hidden = false;
488
- dom.footerPaused.textContent = `paused: ${state.pausedBuffer.length} buffered`;
747
+ dom.footerPaused.textContent = `pausado: ${state.pausedBuffer.length} em fila`;
489
748
  return;
490
749
  }
491
750
  pushVisibleEvent(evt);
@@ -494,6 +753,144 @@
494
753
  }
495
754
  }
496
755
 
756
+ // ------- Active runs ---------------------------------------------------
757
+ // Maintain a Map<runId, ActiveRun> that tracks currently-executing tools.
758
+ // Updated from run.start / progress / run.end / error events that share a
759
+ // runId. Renders as cards above the event log so the user sees CURRENT
760
+ // state at a glance instead of having to scan the chronological feed.
761
+
762
+ const activeRuns = new Map(); // runId -> {tool, label, percent, current, total, startedAt, status, pendingRemove}
763
+ const fadeTimers = new Map(); // runId -> setTimeout handle
764
+
765
+ function upsertActiveRun(evt) {
766
+ if (!evt.runId) return;
767
+ const id = evt.runId;
768
+ const p = evt.payload || {};
769
+ let run = activeRuns.get(id);
770
+
771
+ if (evt.type === 'run.start') {
772
+ // Cancel any pending fade-out for a stale entry with the same id (rare).
773
+ if (fadeTimers.has(id)) { clearTimeout(fadeTimers.get(id)); fadeTimers.delete(id); }
774
+ activeRuns.set(id, {
775
+ runId: id,
776
+ tool: p.tool || p.server || 'run',
777
+ label: 'iniciando…',
778
+ percent: 0,
779
+ current: null,
780
+ total: null,
781
+ startedAt: evt.ts,
782
+ status: 'running',
783
+ });
784
+ } else if (evt.type === 'progress' && run && run.status === 'running') {
785
+ if (typeof p.percent === 'number') run.percent = clampPercent(p.percent);
786
+ if (typeof p.current === 'number') run.current = p.current;
787
+ if (typeof p.total === 'number') run.total = p.total;
788
+ if (typeof p.label === 'string' && p.label.length > 0) run.label = humanizeLabel(p.label);
789
+ // If the wrapper sent current/total but no percent, derive it.
790
+ if (typeof p.percent !== 'number' && typeof p.current === 'number' && typeof p.total === 'number' && p.total > 0) {
791
+ run.percent = clampPercent(Math.round((p.current / p.total) * 100));
792
+ }
793
+ } else if (evt.type === 'tool_invocation' && run && run.status === 'running') {
794
+ // tool_invocation events refine the title if it arrived after run.start.
795
+ if (typeof p.tool === 'string') run.tool = p.tool;
796
+ } else if (evt.type === 'run.end' && run) {
797
+ run.status = (p && p.ok === false) ? 'error' : 'done';
798
+ run.percent = run.status === 'done' ? 100 : run.percent;
799
+ run.label = run.status === 'done' ? 'concluído com sucesso' : (p?.message || 'falhou');
800
+ // Fade the card out a few seconds after completion so the user sees
801
+ // the 100% and "done" before it disappears.
802
+ const t = setTimeout(() => {
803
+ activeRuns.delete(id);
804
+ fadeTimers.delete(id);
805
+ renderActiveRuns();
806
+ }, 4000);
807
+ fadeTimers.set(id, t);
808
+ } else if (evt.type === 'error' && run && run.status === 'running') {
809
+ run.status = 'error';
810
+ run.label = p?.message || 'error';
811
+ // Errors stay visible longer (8s) so the user has time to read.
812
+ const t = setTimeout(() => {
813
+ activeRuns.delete(id);
814
+ fadeTimers.delete(id);
815
+ renderActiveRuns();
816
+ }, 8000);
817
+ fadeTimers.set(id, t);
818
+ }
819
+ renderActiveRuns();
820
+ }
821
+
822
+ function clampPercent(n) {
823
+ if (!Number.isFinite(n)) return 0;
824
+ return Math.max(0, Math.min(100, n));
825
+ }
826
+
827
+ function fmtElapsed(startTs) {
828
+ const sec = Math.max(0, Math.round((Date.now() - startTs) / 1000));
829
+ if (sec < 60) return `${sec}s`;
830
+ const m = Math.floor(sec / 60);
831
+ const s = sec % 60;
832
+ return `${m}m ${s.toString().padStart(2, '0')}s`;
833
+ }
834
+
835
+ function renderActiveRuns() {
836
+ const runs = [...activeRuns.values()].sort((a, b) => a.startedAt - b.startedAt);
837
+ if (runs.length === 0) {
838
+ dom.activeRuns.hidden = true;
839
+ dom.activeRunsList.replaceChildren();
840
+ return;
841
+ }
842
+ dom.activeRuns.hidden = false;
843
+ dom.activeRunsCount.textContent = String(runs.length);
844
+
845
+ // Use a stable key (runId) so we update existing cards in place rather than
846
+ // recreating them (preserves the CSS transition on the progress bar width).
847
+ const existing = new Map();
848
+ for (const child of dom.activeRunsList.children) {
849
+ existing.set(child.dataset.runid, child);
850
+ }
851
+
852
+ const frag = document.createDocumentFragment();
853
+ for (const run of runs) {
854
+ let card = existing.get(run.runId);
855
+ if (!card) {
856
+ card = el('div', { class: 'run-card', data: { runid: run.runId } });
857
+ card.innerHTML = `
858
+ <div class="run-title">
859
+ <span class="run-name"></span>
860
+ <span class="run-kind"></span>
861
+ </div>
862
+ <div class="run-percent"></div>
863
+ <div class="run-bar-wrap"><div class="run-bar"></div></div>
864
+ <div class="run-step"></div>
865
+ <div class="run-meta"><span class="elapsed"></span><span class="runid"></span></div>
866
+ `;
867
+ }
868
+ card.dataset.status = run.status;
869
+ card.querySelector('.run-name').textContent = humanizeTool(run.tool);
870
+ card.querySelector('.run-kind').textContent = humanizeStatus(run.status);
871
+ card.querySelector('.run-percent').textContent = run.status === 'error' ? 'ERRO' : `${run.percent}%`;
872
+ card.querySelector('.run-bar').style.width = `${run.percent}%`;
873
+ card.querySelector('.run-step').textContent = run.label;
874
+ const elapsed = card.querySelector('.elapsed');
875
+ elapsed.textContent = `há ${fmtElapsed(run.startedAt)}`;
876
+ const ridEl = card.querySelector('.runid');
877
+ const progressTotalText = (run.current !== null && run.total !== null) ? ` · ${run.current}/${run.total}` : '';
878
+ ridEl.textContent = `id ${run.runId.slice(0, 8)}${progressTotalText}`;
879
+ frag.appendChild(card);
880
+ }
881
+ dom.activeRunsList.replaceChildren(frag);
882
+ }
883
+
884
+ // Tick the elapsed-time labels every second so they stay live.
885
+ setInterval(() => {
886
+ if (activeRuns.size === 0) return;
887
+ for (const card of dom.activeRunsList.children) {
888
+ const id = card.dataset.runid;
889
+ const run = activeRuns.get(id);
890
+ if (run) card.querySelector('.elapsed').textContent = `há ${fmtElapsed(run.startedAt)}`;
891
+ }
892
+ }, 1000);
893
+
497
894
  function flushPaused() {
498
895
  for (const evt of state.pausedBuffer) pushVisibleEvent(evt);
499
896
  state.pausedBuffer.length = 0;
@@ -510,7 +907,9 @@
510
907
  else state.typeFilter.delete(t);
511
908
  applyFilter();
512
909
  });
513
- const lbl = el('label', {}, [cb, document.createTextNode(t)]);
910
+ // Show humanized text but keep the data-type as the raw event name so
911
+ // power users (and tests) can still target the underlying value.
912
+ const lbl = el('label', { data: { type: t } }, [cb, document.createTextNode(humanizeEventType(t))]);
514
913
  dom.typeFilters.appendChild(lbl);
515
914
  }
516
915
 
@@ -522,7 +921,7 @@
522
921
  dom.pauseBtn.addEventListener('click', () => {
523
922
  state.paused = !state.paused;
524
923
  dom.pauseBtn.setAttribute('aria-pressed', String(state.paused));
525
- dom.pauseBtn.textContent = state.paused ? '▶ resume' : '⏸ pause';
924
+ dom.pauseBtn.textContent = state.paused ? '▶ retomar' : '⏸ pausar';
526
925
  if (!state.paused) flushPaused();
527
926
  });
528
927
 
@@ -545,7 +944,7 @@
545
944
 
546
945
  function setConnState(s) {
547
946
  dom.conn.dataset.state = s;
548
- dom.connText.textContent = s;
947
+ dom.connText.textContent = CONN_LABEL[s] || s;
549
948
  }
550
949
 
551
950
  function scheduleClosedBanner() {
@@ -563,7 +962,7 @@
563
962
  const res = await fetch('/state', { credentials: 'omit' });
564
963
  if (!res.ok) return;
565
964
  const j = await res.json();
566
- if (j.port) dom.metaPort.textContent = `port ${j.port}`;
965
+ if (j.port) dom.metaPort.textContent = `porta ${j.port}`;
567
966
  if (Array.isArray(j.events)) {
568
967
  for (const evt of j.events) ingestEvent(evt);
569
968
  }
@@ -598,6 +997,20 @@
598
997
  evtSource.onmessage = handler; // fallback for events without type field
599
998
  }
600
999
 
1000
+ // ------- Background-tab recovery -------------------------------------
1001
+ // Chrome aggressively throttles background tabs; the native EventSource
1002
+ // retry timer can get suspended, leaving the connection stuck in CLOSED
1003
+ // when the user comes back. The Page Visibility API lets us force a fresh
1004
+ // connect() when the tab becomes visible again and we know we're CLOSED.
1005
+ document.addEventListener('visibilitychange', () => {
1006
+ if (document.visibilityState !== 'visible') return;
1007
+ if (dom.conn.dataset.state === 'CLOSED') {
1008
+ // Re-hydrate from /state in case events arrived while we were dropped,
1009
+ // then reopen the SSE stream.
1010
+ hydrateFromState().then(connect);
1011
+ }
1012
+ });
1013
+
601
1014
  // ------- Boot ---------------------------------------------------------
602
1015
 
603
1016
  hydrateFromState().then(() => {