@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 +80 -0
- package/package.json +1 -1
- package/src/ui/static/index.html +436 -23
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.
|
|
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": {
|
package/src/ui/static/index.html
CHANGED
|
@@ -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">
|
|
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">
|
|
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="
|
|
319
|
-
<fieldset class="filters" id="type-filters" aria-label="
|
|
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">⏸
|
|
323
|
-
<button id="autoscroll-btn" aria-pressed="true">↧
|
|
324
|
-
<button id="clear-btn" title="
|
|
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
|
-
|
|
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">
|
|
341
|
-
<span id="footer-paused" hidden>
|
|
342
|
-
<span id="footer-source">
|
|
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;
|
|
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.
|
|
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
|
-
|
|
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 = `
|
|
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 = `
|
|
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
|
-
|
|
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 ? '▶
|
|
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 = `
|
|
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(() => {
|