@luanpdd/kit-mcp 1.4.0 → 1.5.1
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 +49 -0
- package/package.json +1 -1
- package/src/ui/static/index.html +469 -148
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,55 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.5.1] - 2026-05-05
|
|
10
|
+
|
|
11
|
+
Patch da UI sidecar: auto-reconnect quando o server reinicia + bordas com respiro.
|
|
12
|
+
|
|
13
|
+
### Corrigido
|
|
14
|
+
|
|
15
|
+
- **UI fica presa em "desconectado" quando server volta.** O `EventSource` nativo às vezes estagna no estado `CONNECTING` mesmo depois do server voltar — usuário precisava recarregar a aba. Agora um poll do `/healthz` a cada 3s roda em paralelo: ao detectar 200, fecha o `EventSource` antigo, hidrata `/state`, e abre um novo. Funciona pra qualquer cenário (kill -9, `kit ui stop` + `kit ui start`, network blip, máquina suspended). Usuário **não precisa mais recarregar** — basta o server voltar.
|
|
16
|
+
|
|
17
|
+
- **Banner "Sidecar encerrou" persistia mesmo após reconnect.** Race entre o handler de shutdown e o poll de saúde podia deixar o banner visível mesmo com a conexão de volta. Agora `applyConnState("open")` sempre remove o banner — estado saudável significa que o aviso está stale.
|
|
18
|
+
|
|
19
|
+
- **Cropping nas bordas da timeline:** "há 22m" colado na borda esquerda e `runId`/tokens-chip cortados na direita. `.tl-row` ganhou `padding: var(--pad-tight) 12px`. `.tl-time` virou `padding-right: 8px`. `.tl-content` ganhou `padding-right: 4px` + `overflow: hidden`. Tokens-chip e tl-runid agora têm `flex-shrink: 0` explícito pra não encolher quando a mensagem ocupa muita largura.
|
|
20
|
+
|
|
21
|
+
### Sem mudanças de API
|
|
22
|
+
|
|
23
|
+
Patch puro de UI. `src/ui/static/index.html` e `test/integration/ui-static.test.js` apenas. Stable API v1.0+ preservada.
|
|
24
|
+
|
|
25
|
+
## [1.5.0] - 2026-05-05
|
|
26
|
+
|
|
27
|
+
UI sidecar — bug fixes visuais + tokens + histórico de sessão.
|
|
28
|
+
|
|
29
|
+
### Adicionado
|
|
30
|
+
|
|
31
|
+
- **Tokens chip** em cada row da timeline e card de active run quando o evento traz `payload.tokens` (também aceita `payload.usage.total_tokens` e `payload.cost.tokens` para compatibilidade com diferentes wrappers). Formato `1.2k` / `5.3k` / `1.5M`.
|
|
32
|
+
- **Soma cumulativa de tokens da sessão** no footer (`6.2k tokens nesta sessão`). Aparece só quando algum evento veio com tokens — quem não usa LLM continua vendo o footer enxuto.
|
|
33
|
+
- **Histórico desta sessão** — drawer flutuante (botão de relógio na toolbar). Persiste em `sessionStorage` (não cross-tab, não cross-session); cada run terminada vira uma row com status (✓/✗/·), título, timestamp, duração, tokens, contagem de eventos e runId truncado. Click expande pra mostrar até os 100 últimos eventos da run com %, label e tokens. Cap em 50 runs (mais antigos são descartados).
|
|
34
|
+
- **Footer mostra runs concluídas** total da sessão (`3 runs concluídas`).
|
|
35
|
+
|
|
36
|
+
### Corrigido
|
|
37
|
+
|
|
38
|
+
- **Mojibake (`�`) em payloads** — eventos publicados via shells com locale ruim podiam vazar U+FFFD. Helper `safeStr()` agora limpa esses bytes antes de qualquer renderização.
|
|
39
|
+
- **Rows vazias na timeline** — `milestone` event que vinha com `payload.label` mas sem `payload.name` rendia em branco. Cascata defensiva agora tenta `name → title → label → name → tipo humanizado` em todos os tipos. Garantia: nenhuma row sai sem texto.
|
|
40
|
+
- **Active card sem título** — antes mostrava `—` sozinho se `payload.tool` estava vazio. Helper `runTitle(run)` cascata `humanizeTool(tool) → lastTitle → lastLabel → lastName → "Processo"`.
|
|
41
|
+
- **Tool inline mostrava `—` em vez de fallback** — `escapeHtml(safeStr(run.tool) || "processo")` no rc-tool, `escapeHtml(safeStr(run.tool) || "")` no rc-foot.
|
|
42
|
+
|
|
43
|
+
### Removido
|
|
44
|
+
|
|
45
|
+
- **Painel "Cenário (mock)" do Tweaks** — apenas cenários reais agora; sem botões de demo.
|
|
46
|
+
- **Botão "▸ replay"** — dependia de mock.
|
|
47
|
+
- **Funções `scenarioSync` / `scenarioMulti` / `scenarioError` / `scenarioIdle` / `runScenario` / `mockTimers` / `later` / `clearMock`** — toda infraestrutura de mock event generator (~80 LOC). `EventSource('/events')` é a única fonte de verdade.
|
|
48
|
+
- **Fallback `file://` boot** — sidecar não é mais aberto via `file://`; só via servidor.
|
|
49
|
+
|
|
50
|
+
### Sem mudanças de API runtime
|
|
51
|
+
|
|
52
|
+
Mudanças concentradas em `src/ui/static/index.html`. `src/core/`, `src/cli/`, `src/mcp-server/`, `src/ui/server.js` intocados. Stable API v1.0+ preservada.
|
|
53
|
+
|
|
54
|
+
### Migration
|
|
55
|
+
|
|
56
|
+
Wrappers que quiserem expor cost/tokens podem agora popular `payload.tokens` (number) em qualquer evento. Quem não popula continua funcionando idêntico — chips não aparecem, footer não mostra a linha de tokens. Histórico é per-tab via `sessionStorage` — fechar a aba apaga (intencional, não persistir é a feature).
|
|
57
|
+
|
|
9
58
|
## [1.4.0] - 2026-05-05
|
|
10
59
|
|
|
11
60
|
Framework velocity — 7 melhorias para os comandos / agentes do kit, focadas em reduzir fricção, evitar conflitos com main, e auto-detectar configs que hoje exigem env var manual.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luanpdd/kit-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
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
|
@@ -483,9 +483,9 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
483
483
|
|
|
484
484
|
.tl-row {
|
|
485
485
|
display: grid;
|
|
486
|
-
grid-template-columns: 64px 18px 1fr;
|
|
486
|
+
grid-template-columns: 64px 18px minmax(0, 1fr);
|
|
487
487
|
gap: 0;
|
|
488
|
-
padding: var(--pad-tight)
|
|
488
|
+
padding: var(--pad-tight) 12px;
|
|
489
489
|
position: relative;
|
|
490
490
|
border-radius: 4px;
|
|
491
491
|
transition: background .15s var(--ease);
|
|
@@ -514,8 +514,9 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
514
514
|
font-size: 11px;
|
|
515
515
|
color: var(--text-3);
|
|
516
516
|
font-variant-numeric: tabular-nums;
|
|
517
|
-
padding-left: 4px;
|
|
518
517
|
padding-top: 1px;
|
|
518
|
+
padding-right: 8px;
|
|
519
|
+
text-align: left;
|
|
519
520
|
}
|
|
520
521
|
|
|
521
522
|
/* rail with node */
|
|
@@ -552,14 +553,17 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
552
553
|
.tl-row[data-type="progress"] .tl-node { width: 5px; height: 5px; margin-top: 6px; }
|
|
553
554
|
|
|
554
555
|
/* group runId connector — visually subtle indent */
|
|
555
|
-
.tl-row[data-grouped="true"] { padding-left: 0; }
|
|
556
556
|
.tl-row[data-grouped="true"] .tl-node { background: var(--text-3); }
|
|
557
557
|
|
|
558
558
|
.tl-content {
|
|
559
559
|
display: flex; align-items: baseline; gap: 8px;
|
|
560
560
|
padding-left: 8px;
|
|
561
|
+
padding-right: 4px;
|
|
561
562
|
min-width: 0;
|
|
563
|
+
overflow: hidden;
|
|
562
564
|
}
|
|
565
|
+
.tl-content > .tokens-chip,
|
|
566
|
+
.tl-content > .tl-runid { flex-shrink: 0; }
|
|
563
567
|
|
|
564
568
|
.tl-badge {
|
|
565
569
|
font-family: var(--mono);
|
|
@@ -802,6 +806,154 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
802
806
|
}
|
|
803
807
|
.tw-actions button:hover { color: var(--text); background: var(--surface-3); }
|
|
804
808
|
|
|
809
|
+
/* ───────────── tokens chips ───────────── */
|
|
810
|
+
.tokens-chip {
|
|
811
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
812
|
+
font-family: var(--mono);
|
|
813
|
+
font-size: 11px;
|
|
814
|
+
color: var(--text-2);
|
|
815
|
+
padding: 1px 6px;
|
|
816
|
+
border: 1px solid var(--line);
|
|
817
|
+
border-radius: 999px;
|
|
818
|
+
background: var(--surface-2);
|
|
819
|
+
font-variant-numeric: tabular-nums;
|
|
820
|
+
white-space: nowrap;
|
|
821
|
+
}
|
|
822
|
+
.tokens-chip::before {
|
|
823
|
+
content: "⌬";
|
|
824
|
+
color: var(--text-3);
|
|
825
|
+
font-size: 10px;
|
|
826
|
+
}
|
|
827
|
+
.rc-tokens {
|
|
828
|
+
margin-top: 6px;
|
|
829
|
+
font-family: var(--mono);
|
|
830
|
+
color: var(--text-3);
|
|
831
|
+
font-size: 11px;
|
|
832
|
+
display: flex; gap: 14px;
|
|
833
|
+
align-items: center;
|
|
834
|
+
}
|
|
835
|
+
.rc-tokens .num { color: var(--text-2); font-variant-numeric: tabular-nums; }
|
|
836
|
+
|
|
837
|
+
/* ───────────── history drawer ───────────── */
|
|
838
|
+
.history {
|
|
839
|
+
position: fixed;
|
|
840
|
+
right: 16px; bottom: 16px;
|
|
841
|
+
width: 360px;
|
|
842
|
+
max-height: 70vh;
|
|
843
|
+
background: var(--surface-2);
|
|
844
|
+
border: 1px solid var(--line-strong);
|
|
845
|
+
border-radius: 12px;
|
|
846
|
+
box-shadow: 0 24px 60px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.02);
|
|
847
|
+
z-index: 50;
|
|
848
|
+
overflow: hidden;
|
|
849
|
+
display: none;
|
|
850
|
+
flex-direction: column;
|
|
851
|
+
animation: tweaks-in .25s var(--ease);
|
|
852
|
+
}
|
|
853
|
+
.history.open { display: flex; }
|
|
854
|
+
.history h3 {
|
|
855
|
+
margin: 0;
|
|
856
|
+
padding: 12px 14px;
|
|
857
|
+
font-family: var(--mono);
|
|
858
|
+
font-size: 10px;
|
|
859
|
+
text-transform: uppercase;
|
|
860
|
+
letter-spacing: .14em;
|
|
861
|
+
color: var(--text-3);
|
|
862
|
+
display: flex; align-items: center; gap: 8px;
|
|
863
|
+
border-bottom: 1px solid var(--line);
|
|
864
|
+
flex-shrink: 0;
|
|
865
|
+
}
|
|
866
|
+
.history h3 .hist-meta {
|
|
867
|
+
color: var(--text-2);
|
|
868
|
+
font-size: 10px;
|
|
869
|
+
font-weight: 400;
|
|
870
|
+
text-transform: none;
|
|
871
|
+
letter-spacing: .04em;
|
|
872
|
+
margin-left: auto;
|
|
873
|
+
}
|
|
874
|
+
.history h3 .close {
|
|
875
|
+
width: 18px; height: 18px;
|
|
876
|
+
display: grid; place-items: center;
|
|
877
|
+
border-radius: 4px;
|
|
878
|
+
color: var(--text-3);
|
|
879
|
+
}
|
|
880
|
+
.history h3 .close:hover { background: var(--surface-3); color: var(--text); }
|
|
881
|
+
.history-body { padding: 8px; overflow-y: auto; flex: 1; }
|
|
882
|
+
.hist-empty {
|
|
883
|
+
padding: 24px 12px;
|
|
884
|
+
text-align: center;
|
|
885
|
+
color: var(--text-3);
|
|
886
|
+
font-size: 12px;
|
|
887
|
+
}
|
|
888
|
+
.hist-row {
|
|
889
|
+
display: grid;
|
|
890
|
+
grid-template-columns: 22px 1fr auto;
|
|
891
|
+
gap: 4px 10px;
|
|
892
|
+
padding: 10px;
|
|
893
|
+
border: 1px solid var(--line);
|
|
894
|
+
border-radius: 8px;
|
|
895
|
+
margin-bottom: 6px;
|
|
896
|
+
background: var(--surface-1);
|
|
897
|
+
transition: border-color .15s var(--ease);
|
|
898
|
+
cursor: pointer;
|
|
899
|
+
}
|
|
900
|
+
.hist-row:hover { border-color: var(--line-strong); background: var(--surface-3); }
|
|
901
|
+
.hist-row[data-status="error"] { border-left: 3px solid var(--err); }
|
|
902
|
+
.hist-row[data-status="ok"] { border-left: 3px solid var(--accent); }
|
|
903
|
+
.hist-status {
|
|
904
|
+
width: 18px; height: 18px;
|
|
905
|
+
display: grid; place-items: center;
|
|
906
|
+
border-radius: 999px;
|
|
907
|
+
font-family: var(--mono);
|
|
908
|
+
font-size: 10px;
|
|
909
|
+
font-weight: 700;
|
|
910
|
+
grid-row: 1 / span 1;
|
|
911
|
+
}
|
|
912
|
+
.hist-row[data-status="ok"] .hist-status { color: var(--accent); background: var(--accent-soft); }
|
|
913
|
+
.hist-row[data-status="error"] .hist-status { color: var(--err); background: var(--err-soft); }
|
|
914
|
+
.hist-row[data-status="running"] .hist-status { color: var(--text-2); background: var(--surface-3); }
|
|
915
|
+
.hist-title {
|
|
916
|
+
font-size: 13px;
|
|
917
|
+
color: var(--text);
|
|
918
|
+
font-weight: 500;
|
|
919
|
+
min-width: 0;
|
|
920
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
921
|
+
}
|
|
922
|
+
.hist-when {
|
|
923
|
+
font-family: var(--mono);
|
|
924
|
+
font-size: 10px;
|
|
925
|
+
color: var(--text-3);
|
|
926
|
+
text-align: right;
|
|
927
|
+
grid-row: 1 / span 1;
|
|
928
|
+
}
|
|
929
|
+
.hist-meta-row {
|
|
930
|
+
grid-column: 2 / span 2;
|
|
931
|
+
display: flex; gap: 12px; flex-wrap: wrap;
|
|
932
|
+
font-family: var(--mono);
|
|
933
|
+
font-size: 10px;
|
|
934
|
+
color: var(--text-3);
|
|
935
|
+
}
|
|
936
|
+
.hist-meta-row .num { color: var(--text-2); font-variant-numeric: tabular-nums; }
|
|
937
|
+
.hist-detail {
|
|
938
|
+
grid-column: 1 / -1;
|
|
939
|
+
margin-top: 6px;
|
|
940
|
+
padding: 8px;
|
|
941
|
+
border: 1px solid var(--line);
|
|
942
|
+
border-radius: 6px;
|
|
943
|
+
background: var(--surface-2);
|
|
944
|
+
font-family: var(--mono);
|
|
945
|
+
font-size: 11px;
|
|
946
|
+
color: var(--text-2);
|
|
947
|
+
max-height: 240px;
|
|
948
|
+
overflow-y: auto;
|
|
949
|
+
display: none;
|
|
950
|
+
}
|
|
951
|
+
.hist-row.expanded .hist-detail { display: block; }
|
|
952
|
+
.hist-detail-row { padding: 2px 0; display: flex; gap: 6px; }
|
|
953
|
+
.hist-detail-row .pct { color: var(--accent); flex-shrink: 0; min-width: 36px; font-variant-numeric: tabular-nums; }
|
|
954
|
+
.hist-detail-row .lbl { color: var(--text-2); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
955
|
+
.hist-detail-row .tok { color: var(--text-3); flex-shrink: 0; }
|
|
956
|
+
|
|
805
957
|
/* density-driven log padding handled via --pad-tight above */
|
|
806
958
|
|
|
807
959
|
/* ───────────────────────── reduced motion ───────────────────────── */
|
|
@@ -868,6 +1020,11 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
868
1020
|
<rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/>
|
|
869
1021
|
</svg>
|
|
870
1022
|
</button>
|
|
1023
|
+
<button class="iconbtn" id="history-btn" aria-pressed="false" aria-label="Histórico desta sessão" title="Histórico desta sessão">
|
|
1024
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1025
|
+
<path d="M3 12a9 9 0 1 0 3-7"/><path d="M3 4v5h5"/><path d="M12 7v5l3 2"/>
|
|
1026
|
+
</svg>
|
|
1027
|
+
</button>
|
|
871
1028
|
<button class="iconbtn" id="tweaks-btn" aria-pressed="false" aria-label="Tweaks" title="Tweaks">
|
|
872
1029
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
873
1030
|
<circle cx="12" cy="12" r="3"/>
|
|
@@ -910,6 +1067,10 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
910
1067
|
<span>fonte: <span class="live" id="src-label">ao vivo</span></span>
|
|
911
1068
|
<span class="sep">·</span>
|
|
912
1069
|
<span id="last-seen">aguardando…</span>
|
|
1070
|
+
<span class="sep" id="footer-tokens-sep" hidden>·</span>
|
|
1071
|
+
<span id="footer-tokens" hidden>0 tokens</span>
|
|
1072
|
+
<span class="sep">·</span>
|
|
1073
|
+
<span id="footer-runs">0 runs concluídas</span>
|
|
913
1074
|
</footer>
|
|
914
1075
|
|
|
915
1076
|
</div>
|
|
@@ -953,23 +1114,26 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
953
1114
|
</div>
|
|
954
1115
|
</div>
|
|
955
1116
|
|
|
956
|
-
<div class="tw-group">
|
|
957
|
-
<span class="tw-label">Cenário (mock)</span>
|
|
958
|
-
<div class="seg" id="tw-scenario">
|
|
959
|
-
<button data-v="sync" aria-pressed="true">Sync</button>
|
|
960
|
-
<button data-v="multi">Multi</button>
|
|
961
|
-
<button data-v="error">Erro</button>
|
|
962
|
-
<button data-v="idle">Idle</button>
|
|
963
|
-
</div>
|
|
964
|
-
</div>
|
|
965
|
-
|
|
966
1117
|
<div class="tw-actions">
|
|
967
|
-
<button id="tw-
|
|
968
|
-
<button id="tw-clear">limpar</button>
|
|
1118
|
+
<button id="tw-clear">limpar tela</button>
|
|
969
1119
|
</div>
|
|
970
1120
|
</div>
|
|
971
1121
|
</aside>
|
|
972
1122
|
|
|
1123
|
+
<!-- HISTORY DRAWER -->
|
|
1124
|
+
<aside class="history" id="history" role="dialog" aria-label="Histórico da sessão">
|
|
1125
|
+
<h3>
|
|
1126
|
+
Histórico desta sessão
|
|
1127
|
+
<span class="hist-meta" id="hist-meta">0 runs</span>
|
|
1128
|
+
<button class="close" id="history-close" aria-label="Fechar">
|
|
1129
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
|
1130
|
+
</button>
|
|
1131
|
+
</h3>
|
|
1132
|
+
<div class="history-body" id="history-body">
|
|
1133
|
+
<div class="hist-empty">Nada por aqui ainda. Quando uma run terminar, ela aparece aqui.</div>
|
|
1134
|
+
</div>
|
|
1135
|
+
</aside>
|
|
1136
|
+
|
|
973
1137
|
<!-- SVG sprite (in-doc, no external) -->
|
|
974
1138
|
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
975
1139
|
<defs>
|
|
@@ -1057,11 +1221,13 @@ function clockTime(ts) {
|
|
|
1057
1221
|
/* ---------- state ---------- */
|
|
1058
1222
|
const state = {
|
|
1059
1223
|
events: [], // newest first
|
|
1060
|
-
runs: new Map(), // runId → { startTs, tool, lastProgress, lastLabel, ended, ok, current, total }
|
|
1224
|
+
runs: new Map(), // runId → { startTs, tool, lastProgress, lastLabel, ended, ok, current, total, tokens, endTs }
|
|
1061
1225
|
filterText: "",
|
|
1062
1226
|
// tool_invocation + shutdown não têm chip de filtro, mas devem aparecer por default
|
|
1063
1227
|
filters: new Set(["run.start","progress","milestone","run.end","error","tool_invocation","shutdown"]),
|
|
1064
1228
|
paused: false,
|
|
1229
|
+
totalTokens: 0, // soma de payload.tokens em TODOS os eventos da sessão
|
|
1230
|
+
history: [], // runs já concluídas neste tab — persistido em sessionStorage
|
|
1065
1231
|
};
|
|
1066
1232
|
|
|
1067
1233
|
/* ---------- DOM refs ---------- */
|
|
@@ -1085,6 +1251,14 @@ const els = {
|
|
|
1085
1251
|
tweaks: $("#tweaks"),
|
|
1086
1252
|
tweaksClose: $("#tweaks-close"),
|
|
1087
1253
|
srcLabel: $("#src-label"),
|
|
1254
|
+
historyBtn: $("#history-btn"),
|
|
1255
|
+
history: $("#history"),
|
|
1256
|
+
historyClose: $("#history-close"),
|
|
1257
|
+
historyBody: $("#history-body"),
|
|
1258
|
+
histMeta: $("#hist-meta"),
|
|
1259
|
+
footerTokens: $("#footer-tokens"),
|
|
1260
|
+
footerTokensSep: $("#footer-tokens-sep"),
|
|
1261
|
+
footerRuns: $("#footer-runs"),
|
|
1088
1262
|
};
|
|
1089
1263
|
|
|
1090
1264
|
/* ---------- ingest one event ---------- */
|
|
@@ -1093,16 +1267,24 @@ function ingest(evt) {
|
|
|
1093
1267
|
state.events.unshift(evt);
|
|
1094
1268
|
if (state.events.length > 200) state.events.length = 200;
|
|
1095
1269
|
|
|
1270
|
+
// tokens — soma global da sessão + por run
|
|
1271
|
+
const tk = readTokens(evt);
|
|
1272
|
+
if (tk > 0) state.totalTokens += tk;
|
|
1273
|
+
|
|
1096
1274
|
// run tracking
|
|
1097
1275
|
if (evt.runId) {
|
|
1098
1276
|
let run = state.runs.get(evt.runId);
|
|
1099
1277
|
if (!run) {
|
|
1100
|
-
run = { runId: evt.runId, startTs: evt.ts, tool: null, lastProgress: 0, lastLabel: "", ended: false, ok: true, current: 0, total: 0, lastTs: evt.ts };
|
|
1278
|
+
run = { runId: evt.runId, startTs: evt.ts, tool: null, lastProgress: 0, lastLabel: "", ended: false, ok: true, current: 0, total: 0, tokens: 0, events: [], lastTs: evt.ts };
|
|
1101
1279
|
state.runs.set(evt.runId, run);
|
|
1102
1280
|
}
|
|
1103
1281
|
run.lastTs = evt.ts;
|
|
1282
|
+
run.tokens = (run.tokens || 0) + tk;
|
|
1283
|
+
// store a slimmed-down record of every event in the run so the history
|
|
1284
|
+
// drawer can show what happened later
|
|
1285
|
+
run.events.push({ ts: evt.ts, type: evt.type, percent: evt.payload?.percent, label: evt.payload?.label, name: evt.payload?.name, message: evt.payload?.message, tokens: tk });
|
|
1104
1286
|
if (evt.type === "run.start") {
|
|
1105
|
-
run.tool = evt.payload?.tool;
|
|
1287
|
+
run.tool = evt.payload?.tool || run.tool;
|
|
1106
1288
|
run.startTs = evt.ts;
|
|
1107
1289
|
} else if (evt.type === "progress") {
|
|
1108
1290
|
if (typeof evt.payload?.percent === "number") run.lastProgress = evt.payload.percent;
|
|
@@ -1112,9 +1294,13 @@ function ingest(evt) {
|
|
|
1112
1294
|
} else if (evt.type === "run.end") {
|
|
1113
1295
|
run.ended = true;
|
|
1114
1296
|
run.ok = !!evt.payload?.ok;
|
|
1297
|
+
run.endTs = evt.ts;
|
|
1115
1298
|
run.lastProgress = run.ok ? 100 : run.lastProgress;
|
|
1299
|
+
// arquive into history (client-only, sessionStorage)
|
|
1300
|
+
archiveRun(run);
|
|
1116
1301
|
} else if (evt.type === "error") {
|
|
1117
|
-
//
|
|
1302
|
+
// não termina o run; só registra
|
|
1303
|
+
run.lastError = evt.payload?.message || "erro";
|
|
1118
1304
|
}
|
|
1119
1305
|
}
|
|
1120
1306
|
|
|
@@ -1122,6 +1308,55 @@ function ingest(evt) {
|
|
|
1122
1308
|
els.lastSeen.textContent = "último: " + clockTime(evt.ts);
|
|
1123
1309
|
}
|
|
1124
1310
|
|
|
1311
|
+
/* ---------- helpers — tokens & history ---------- */
|
|
1312
|
+
function readTokens(evt) {
|
|
1313
|
+
// Aceita várias chaves comuns: payload.tokens (preferido), payload.usage.total_tokens, payload.cost.tokens
|
|
1314
|
+
const p = evt.payload || {};
|
|
1315
|
+
if (typeof p.tokens === "number") return p.tokens;
|
|
1316
|
+
if (p.usage && typeof p.usage.total_tokens === "number") return p.usage.total_tokens;
|
|
1317
|
+
if (p.cost && typeof p.cost.tokens === "number") return p.cost.tokens;
|
|
1318
|
+
return 0;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function fmtTokens(n) {
|
|
1322
|
+
if (n < 1000) return String(n);
|
|
1323
|
+
if (n < 10_000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k";
|
|
1324
|
+
if (n < 1_000_000) return Math.round(n / 1000) + "k";
|
|
1325
|
+
return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function archiveRun(run) {
|
|
1329
|
+
// Snapshot leve da run completa — o que vai pra sessionStorage tem que ser serializável.
|
|
1330
|
+
const entry = {
|
|
1331
|
+
runId: run.runId,
|
|
1332
|
+
tool: run.tool,
|
|
1333
|
+
startTs: run.startTs,
|
|
1334
|
+
endTs: run.endTs,
|
|
1335
|
+
durationMs: (run.endTs || Date.now()) - run.startTs,
|
|
1336
|
+
ok: run.ok,
|
|
1337
|
+
tokens: run.tokens || 0,
|
|
1338
|
+
eventsCount: run.events.length,
|
|
1339
|
+
events: run.events.slice(-100), // últimos 100 eventos da run pra detalhe
|
|
1340
|
+
};
|
|
1341
|
+
state.history.unshift(entry);
|
|
1342
|
+
// cap em 50 runs pra não estourar sessionStorage
|
|
1343
|
+
if (state.history.length > 50) state.history.length = 50;
|
|
1344
|
+
saveHistory();
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function saveHistory() {
|
|
1348
|
+
try {
|
|
1349
|
+
sessionStorage.setItem("kit-mcp-history", JSON.stringify(state.history));
|
|
1350
|
+
} catch (_) { /* quota? sem espaço? sem stress. */ }
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function loadHistory() {
|
|
1354
|
+
try {
|
|
1355
|
+
const raw = sessionStorage.getItem("kit-mcp-history");
|
|
1356
|
+
if (raw) state.history = JSON.parse(raw) || [];
|
|
1357
|
+
} catch (_) { state.history = []; }
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1125
1360
|
/* ---------- render ---------- */
|
|
1126
1361
|
function passesFilter(evt) {
|
|
1127
1362
|
if (!state.filters.has(evt.type)) return false;
|
|
@@ -1137,6 +1372,64 @@ function render() {
|
|
|
1137
1372
|
const total = state.events.length;
|
|
1138
1373
|
els.evtCount.textContent = `${total} evento${total === 1 ? "" : "s"}`;
|
|
1139
1374
|
els.logCount.textContent = `${total} evento${total === 1 ? "" : "s"}`;
|
|
1375
|
+
// Tokens (mostra só quando algum evento da sessão veio com payload.tokens)
|
|
1376
|
+
if (state.totalTokens > 0) {
|
|
1377
|
+
els.footerTokens.hidden = false;
|
|
1378
|
+
els.footerTokensSep.hidden = false;
|
|
1379
|
+
els.footerTokens.textContent = `${fmtTokens(state.totalTokens)} tokens nesta sessão`;
|
|
1380
|
+
} else {
|
|
1381
|
+
els.footerTokens.hidden = true;
|
|
1382
|
+
els.footerTokensSep.hidden = true;
|
|
1383
|
+
}
|
|
1384
|
+
const completed = state.history.length;
|
|
1385
|
+
els.footerRuns.textContent = `${completed} run${completed === 1 ? "" : "s"} concluída${completed === 1 ? "" : "s"}`;
|
|
1386
|
+
if (els.histMeta) els.histMeta.textContent = `${completed} run${completed === 1 ? "" : "s"}`;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/* ---------- history drawer renderer ---------- */
|
|
1390
|
+
function renderHistory() {
|
|
1391
|
+
if (!els.historyBody) return;
|
|
1392
|
+
if (state.history.length === 0) {
|
|
1393
|
+
els.historyBody.innerHTML = `<div class="hist-empty">Nada por aqui ainda. Quando uma run terminar, ela aparece aqui.</div>`;
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
const rows = state.history.map((h) => historyRowHtml(h)).join("");
|
|
1397
|
+
els.historyBody.innerHTML = rows;
|
|
1398
|
+
els.historyBody.querySelectorAll(".hist-row").forEach((row) => {
|
|
1399
|
+
row.addEventListener("click", () => row.classList.toggle("expanded"));
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function historyRowHtml(h) {
|
|
1404
|
+
const status = h.ok === true ? "ok" : h.ok === false ? "error" : "running";
|
|
1405
|
+
const statusGlyph = status === "ok" ? "✓" : status === "error" ? "✕" : "·";
|
|
1406
|
+
const title = humanizeTool(safeStr(h.tool || "")) || safeStr(h.tool) || "Processo";
|
|
1407
|
+
const dur = formatElapsed(h.durationMs || 0);
|
|
1408
|
+
const when = clockTime(h.startTs);
|
|
1409
|
+
const tokens = h.tokens > 0 ? `<span class="num">${fmtTokens(h.tokens)}</span> tokens` : "<span>sem tokens registrados</span>";
|
|
1410
|
+
const detailRows = (h.events || [])
|
|
1411
|
+
.filter((e) => e.type === "progress" || e.type === "milestone" || e.type === "error" || e.type === "run.end")
|
|
1412
|
+
.map((e) => {
|
|
1413
|
+
const pct = typeof e.percent === "number" ? `${Math.round(e.percent)}%` : "·";
|
|
1414
|
+
const lbl = safeStr(e.label) || safeStr(e.name) || safeStr(e.message) || humanizeEventType(e.type);
|
|
1415
|
+
const tk = e.tokens > 0 ? fmtTokens(e.tokens) : "";
|
|
1416
|
+
return `<div class="hist-detail-row"><span class="pct">${escapeHtml(pct)}</span><span class="lbl">${escapeHtml(lbl)}</span>${tk ? `<span class="tok">${tk}t</span>` : ""}</div>`;
|
|
1417
|
+
})
|
|
1418
|
+
.join("");
|
|
1419
|
+
return `
|
|
1420
|
+
<div class="hist-row" data-status="${status}" data-runid="${h.runId}">
|
|
1421
|
+
<div class="hist-status">${statusGlyph}</div>
|
|
1422
|
+
<div class="hist-title">${escapeHtml(title)}</div>
|
|
1423
|
+
<div class="hist-when">${when}</div>
|
|
1424
|
+
<div class="hist-meta-row">
|
|
1425
|
+
<span><span class="num">${dur}</span> dur</span>
|
|
1426
|
+
<span>${tokens}</span>
|
|
1427
|
+
<span><span class="num">${h.eventsCount || 0}</span> eventos</span>
|
|
1428
|
+
<span class="num">id ${h.runId.slice(0,8)}</span>
|
|
1429
|
+
</div>
|
|
1430
|
+
<div class="hist-detail">${detailRows || '<div class="hist-detail-row"><span class="lbl">(sem progresso registrado)</span></div>'}</div>
|
|
1431
|
+
</div>
|
|
1432
|
+
`;
|
|
1140
1433
|
}
|
|
1141
1434
|
|
|
1142
1435
|
function renderActive() {
|
|
@@ -1165,18 +1458,44 @@ function renderActive() {
|
|
|
1165
1458
|
});
|
|
1166
1459
|
}
|
|
1167
1460
|
|
|
1461
|
+
/**
|
|
1462
|
+
* Sanitização: remove o "U+FFFD replacement character" e variantes que
|
|
1463
|
+
* indicam mojibake — eventos vindos de publishers em locale ruim podem
|
|
1464
|
+
* vazar bytes corrompidos. Em vez de mostrar `�`, devolvemos string vazia
|
|
1465
|
+
* e deixamos o fallback escolher um nome decente.
|
|
1466
|
+
*/
|
|
1467
|
+
function safeStr(s) {
|
|
1468
|
+
if (typeof s !== "string") return "";
|
|
1469
|
+
// remove FFFD (U+FFFD) e null bytes
|
|
1470
|
+
const cleaned = s.replace(/�+/g, "").replace(/+/g, "").trim();
|
|
1471
|
+
return cleaned;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Resolve o melhor título legível para uma run, NUNCA devolve "—" sozinho.
|
|
1476
|
+
* Cascata: humanizeTool(payload.tool) → payload.title → payload.label →
|
|
1477
|
+
* payload.name → "Processo".
|
|
1478
|
+
*/
|
|
1479
|
+
function runTitle(run) {
|
|
1480
|
+
const t = humanizeTool(safeStr(run.tool || ""));
|
|
1481
|
+
if (t && t !== "—") return t;
|
|
1482
|
+
const fallback = safeStr(run.lastTitle) || safeStr(run.lastLabel) || safeStr(run.lastName);
|
|
1483
|
+
if (fallback) return fallback;
|
|
1484
|
+
return "Processo";
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1168
1487
|
function updateActiveCard(card, run) {
|
|
1169
1488
|
const family = TOOL_FAMILIES[run.tool] || "sync";
|
|
1170
1489
|
const percent = Math.max(0, Math.min(100, Math.round(run.lastProgress)));
|
|
1171
1490
|
const elapsed = Date.now() - run.startTs;
|
|
1172
1491
|
const longRunning = elapsed > 30_000;
|
|
1173
|
-
const stepLabel = humanizePath(run.lastLabel) || "iniciando…";
|
|
1492
|
+
const stepLabel = humanizePath(safeStr(run.lastLabel)) || "iniciando…";
|
|
1174
1493
|
const stepCount = run.total ? `${run.current}/${run.total}` : "";
|
|
1175
1494
|
|
|
1176
1495
|
const set = (sel, fn) => { const n = card.querySelector(sel); if (n) fn(n); };
|
|
1177
1496
|
set(".rc-icon", (n) => { n.dataset.tool = family; n.querySelector("use")?.setAttribute("href", `#i-${family}`); });
|
|
1178
|
-
set(".rc-tool", (n) => { n.textContent = run.tool || "
|
|
1179
|
-
set(".rc-title", (n) => { n.textContent =
|
|
1497
|
+
set(".rc-tool", (n) => { n.textContent = safeStr(run.tool) || "processo"; });
|
|
1498
|
+
set(".rc-title", (n) => { n.textContent = runTitle(run); });
|
|
1180
1499
|
set(".rc-bar-fill", (n) => { n.style.width = percent + "%"; });
|
|
1181
1500
|
set(".rc-pct", (n) => { n.textContent = percent + "%"; });
|
|
1182
1501
|
set(".rc-step-text", (n) => { if (n.textContent !== stepLabel) n.textContent = stepLabel; });
|
|
@@ -1184,24 +1503,27 @@ function updateActiveCard(card, run) {
|
|
|
1184
1503
|
set(".rc-elapsed-val", (n) => { n.textContent = formatElapsed(elapsed); });
|
|
1185
1504
|
set(".rc-elapsed .em", (n) => n.classList.toggle("warn", longRunning));
|
|
1186
1505
|
set(".rc-runid", (n) => { n.textContent = "id " + run.runId.slice(0, 8); });
|
|
1506
|
+
set(".rc-tokens-val", (n) => { n.textContent = run.tokens > 0 ? fmtTokens(run.tokens) : "—"; });
|
|
1507
|
+
set(".rc-tokens", (n) => { n.classList.toggle("hidden", !(run.tokens > 0)); });
|
|
1187
1508
|
}
|
|
1188
1509
|
|
|
1189
1510
|
function activeCardHtml(run) {
|
|
1190
1511
|
const family = TOOL_FAMILIES[run.tool] || "sync";
|
|
1191
1512
|
const iconHref = `#i-${family}`;
|
|
1192
|
-
const title =
|
|
1193
|
-
const stepLabel = humanizePath(run.lastLabel) || "iniciando…";
|
|
1513
|
+
const title = runTitle(run);
|
|
1514
|
+
const stepLabel = humanizePath(safeStr(run.lastLabel)) || "iniciando…";
|
|
1194
1515
|
const percent = Math.max(0, Math.min(100, Math.round(run.lastProgress)));
|
|
1195
1516
|
const elapsed = Date.now() - run.startTs;
|
|
1196
1517
|
const longRunning = elapsed > 30_000;
|
|
1197
1518
|
const stepCount = run.total ? `${run.current}/${run.total}` : "";
|
|
1519
|
+
const showTokens = run.tokens > 0;
|
|
1198
1520
|
return `
|
|
1199
1521
|
<article class="run-card" data-runid="${run.runId}">
|
|
1200
1522
|
<div class="rc-head">
|
|
1201
1523
|
<div class="rc-icon" data-tool="${family}"><svg><use href="${iconHref}"/></svg></div>
|
|
1202
1524
|
<div class="rc-title-block">
|
|
1203
|
-
<div class="rc-tool">${run.tool || "
|
|
1204
|
-
<div class="rc-title">${title}</div>
|
|
1525
|
+
<div class="rc-tool">${escapeHtml(safeStr(run.tool) || "processo")}</div>
|
|
1526
|
+
<div class="rc-title">${escapeHtml(title)}</div>
|
|
1205
1527
|
</div>
|
|
1206
1528
|
<div class="rc-elapsed">
|
|
1207
1529
|
<span class="em ${longRunning ? "warn" : ""}"><span class="rc-elapsed-val">${formatElapsed(elapsed)}</span></span>
|
|
@@ -1220,10 +1542,15 @@ function activeCardHtml(run) {
|
|
|
1220
1542
|
${stepCount ? `<span class="rc-step-count">${stepCount}</span>` : ""}
|
|
1221
1543
|
</div>
|
|
1222
1544
|
|
|
1545
|
+
<div class="rc-tokens" ${showTokens ? "" : "style=\"display:none\""}>
|
|
1546
|
+
<span class="tokens-chip"><span class="rc-tokens-val">${showTokens ? fmtTokens(run.tokens) : "—"}</span></span>
|
|
1547
|
+
<span>tokens nesta run</span>
|
|
1548
|
+
</div>
|
|
1549
|
+
|
|
1223
1550
|
<div class="rc-foot">
|
|
1224
1551
|
<span class="rc-runid">id ${run.runId.slice(0, 8)}</span>
|
|
1225
1552
|
<span class="sep">·</span>
|
|
1226
|
-
<span>${run.tool || ""}</span>
|
|
1553
|
+
<span>${escapeHtml(safeStr(run.tool) || "")}</span>
|
|
1227
1554
|
</div>
|
|
1228
1555
|
</article>
|
|
1229
1556
|
`;
|
|
@@ -1296,39 +1623,71 @@ function rowHtml(evt, idx, prev) {
|
|
|
1296
1623
|
const time = clockTime(evt.ts);
|
|
1297
1624
|
const rel = relTime(Date.now() - evt.ts);
|
|
1298
1625
|
const badge = humanizeEventType(evt.type);
|
|
1299
|
-
const
|
|
1626
|
+
const tk = readTokens(evt);
|
|
1627
|
+
const tokenChip = tk > 0 ? ` <span class="tokens-chip" title="tokens">${fmtTokens(tk)}</span>` : "";
|
|
1628
|
+
/**
|
|
1629
|
+
* Cascata defensiva pra NUNCA renderizar uma row sem texto:
|
|
1630
|
+
* 1) campo específico do tipo (name pra milestone, message pra error, …)
|
|
1631
|
+
* 2) payload.label
|
|
1632
|
+
* 3) payload.title
|
|
1633
|
+
* 4) humanizeTool(payload.tool)
|
|
1634
|
+
* 5) o tipo humanizado em si (último recurso — "Marco", "Erro", "Iniciado")
|
|
1635
|
+
*/
|
|
1636
|
+
const fallback = (...candidates) => {
|
|
1637
|
+
for (const c of candidates) {
|
|
1638
|
+
const s = safeStr(typeof c === "string" ? c : (c == null ? "" : String(c)));
|
|
1639
|
+
if (s) return s;
|
|
1640
|
+
}
|
|
1641
|
+
return badge.toLowerCase();
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1300
1644
|
let msg = "";
|
|
1301
1645
|
switch (evt.type) {
|
|
1302
1646
|
case "run.start": {
|
|
1303
|
-
|
|
1647
|
+
const tool = safeStr(evt.payload?.tool || "");
|
|
1648
|
+
const title = humanizeTool(tool) || fallback(evt.payload?.title, evt.payload?.label, evt.payload?.name);
|
|
1649
|
+
const ident = tool ? ` <span class="ident">${escapeHtml(tool)}</span>` : "";
|
|
1650
|
+
const target = evt.payload?.target ? ` <span class="arrow">→</span> <span class="ident">${escapeHtml(safeStr(evt.payload.target))}</span>` : "";
|
|
1651
|
+
msg = `<strong>${escapeHtml(title)}</strong>${ident}${target}`;
|
|
1304
1652
|
break;
|
|
1305
1653
|
}
|
|
1306
1654
|
case "run.end": {
|
|
1307
1655
|
const dur = evt.payload?.duration_ms;
|
|
1308
|
-
|
|
1656
|
+
const tool = safeStr(evt.payload?.tool || "");
|
|
1657
|
+
const title = humanizeTool(tool) || fallback(evt.payload?.title, evt.payload?.label, evt.payload?.name);
|
|
1658
|
+
msg = `<strong>${escapeHtml(title)}</strong> ${ok ? "concluído" : "falhou"}${dur ? ` <span class="ident">${(dur/1000).toFixed(2)}s</span>` : ""}`;
|
|
1309
1659
|
break;
|
|
1310
1660
|
}
|
|
1311
1661
|
case "progress": {
|
|
1312
1662
|
const pct = typeof evt.payload?.percent === "number" ? `${Math.round(evt.payload.percent)}%` : "";
|
|
1313
|
-
const lbl = evt.payload?.label
|
|
1663
|
+
const lbl = fallback(evt.payload?.label, evt.payload?.title, evt.payload?.name, "em andamento");
|
|
1314
1664
|
const ct = evt.payload?.current && evt.payload?.total ? ` <span class="ident">${evt.payload.current}/${evt.payload.total}</span>` : "";
|
|
1315
1665
|
msg = `${pct ? `<strong>${pct}</strong> ` : ""}<span class="path">${escapeHtml(humanizePath(lbl))}</span>${ct}`;
|
|
1316
1666
|
break;
|
|
1317
1667
|
}
|
|
1318
1668
|
case "milestone": {
|
|
1319
|
-
|
|
1669
|
+
const lbl = fallback(evt.payload?.name, evt.payload?.title, evt.payload?.label, "marco");
|
|
1670
|
+
msg = `<strong>${escapeHtml(lbl)}</strong>`;
|
|
1320
1671
|
break;
|
|
1321
1672
|
}
|
|
1322
1673
|
case "error": {
|
|
1323
|
-
|
|
1674
|
+
const lbl = fallback(evt.payload?.message, evt.payload?.title, evt.payload?.label, evt.payload?.name, "erro");
|
|
1675
|
+
const code = evt.payload?.code ? ` <span class="ident">${escapeHtml(safeStr(evt.payload.code))}</span>` : "";
|
|
1676
|
+
msg = `<strong>${escapeHtml(lbl)}</strong>${code}`;
|
|
1324
1677
|
break;
|
|
1325
1678
|
}
|
|
1326
1679
|
case "tool_invocation": {
|
|
1327
|
-
|
|
1680
|
+
const tool = safeStr(evt.payload?.tool || "");
|
|
1681
|
+
const title = humanizeTool(tool) || fallback(evt.payload?.title, evt.payload?.label, "ferramenta invocada");
|
|
1682
|
+
msg = `<strong>${escapeHtml(title)}</strong>${tool ? ` <span class="ident">${escapeHtml(tool)}</span>` : ""}`;
|
|
1683
|
+
break;
|
|
1684
|
+
}
|
|
1685
|
+
case "shutdown": {
|
|
1686
|
+
msg = `<strong>${escapeHtml(fallback(evt.payload?.message, "sidecar encerrado"))}</strong>`;
|
|
1328
1687
|
break;
|
|
1329
1688
|
}
|
|
1330
1689
|
default:
|
|
1331
|
-
msg = ""
|
|
1690
|
+
msg = `<span class="ident">${escapeHtml(badge.toLowerCase())}</span>`;
|
|
1332
1691
|
}
|
|
1333
1692
|
return `
|
|
1334
1693
|
<div class="tl-row" data-type="${evt.type}" data-ok="${ok}" data-grouped="${grouped}">
|
|
@@ -1337,6 +1696,7 @@ function rowHtml(evt, idx, prev) {
|
|
|
1337
1696
|
<div class="tl-content">
|
|
1338
1697
|
<span class="tl-badge">${badge}</span>
|
|
1339
1698
|
<span class="tl-msg">${msg}</span>
|
|
1699
|
+
${tokenChip}
|
|
1340
1700
|
${evt.runId ? `<span class="tl-runid">${evt.runId.slice(0,6)}</span>` : ""}
|
|
1341
1701
|
</div>
|
|
1342
1702
|
</div>
|
|
@@ -1365,104 +1725,14 @@ setInterval(() => {
|
|
|
1365
1725
|
});
|
|
1366
1726
|
}, 1000);
|
|
1367
1727
|
|
|
1368
|
-
/* ─────────────────────
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
mockTimers.forEach(clearTimeout);
|
|
1372
|
-
mockTimers = [];
|
|
1373
|
-
}
|
|
1374
|
-
function later(ms, fn) { mockTimers.push(setTimeout(fn, ms)); }
|
|
1375
|
-
|
|
1376
|
-
function genRunId() { return Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 8); }
|
|
1377
|
-
|
|
1378
|
-
function scenarioSync(delay = 0) {
|
|
1379
|
-
const runId = genRunId();
|
|
1380
|
-
const tool = "sync.install";
|
|
1381
|
-
const start = Date.now() + delay;
|
|
1382
|
-
const target = "claude-code";
|
|
1383
|
-
later(delay, () => ingest({ type: "run.start", ts: start, runId, payload: { tool, target } }));
|
|
1384
|
-
|
|
1385
|
-
const total = 19;
|
|
1386
|
-
const labels = [
|
|
1387
|
-
"lendo agente planner",
|
|
1388
|
-
"analisando dependências",
|
|
1389
|
-
"projetando framework",
|
|
1390
|
-
"compilando templates",
|
|
1391
|
-
"writing .claude/agents/planner.md",
|
|
1392
|
-
"writing .claude/agents/builder.md",
|
|
1393
|
-
"writing .claude/agents/reviewer.md",
|
|
1394
|
-
"writing .claude/commands/sync.md",
|
|
1395
|
-
"validando schema",
|
|
1396
|
-
"merging com kit base",
|
|
1397
|
-
"writing .claude/agents/coordinator.md",
|
|
1398
|
-
"writing .claude/agents/specialist.md",
|
|
1399
|
-
"configurando hooks",
|
|
1400
|
-
"writing .claude/agents/architect.md",
|
|
1401
|
-
"writing .claude/agents/qa.md",
|
|
1402
|
-
"writing .claude/agents/docs.md",
|
|
1403
|
-
"selando manifest",
|
|
1404
|
-
"rodando linter final",
|
|
1405
|
-
"gravando metadata",
|
|
1406
|
-
];
|
|
1407
|
-
for (let i = 0; i < total; i++) {
|
|
1408
|
-
const t = delay + 220 + i * 540;
|
|
1409
|
-
later(t, () => ingest({
|
|
1410
|
-
type: "progress", ts: Date.now(), runId,
|
|
1411
|
-
payload: { percent: Math.round(((i + 1) / total) * 100), label: labels[i], current: i + 1, total, kind: "fs" }
|
|
1412
|
-
}));
|
|
1413
|
-
}
|
|
1414
|
-
later(delay + 220 + total * 540 + 100, () => ingest({
|
|
1415
|
-
type: "milestone", ts: Date.now(), runId, payload: { name: `✓ ${total} agentes projetados` }
|
|
1416
|
-
}));
|
|
1417
|
-
later(delay + 220 + total * 540 + 600, () => ingest({
|
|
1418
|
-
type: "run.end", ts: Date.now(), runId, payload: { tool, ok: true, duration_ms: 220 + total * 540 + 400 }
|
|
1419
|
-
}));
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
function scenarioMulti() {
|
|
1423
|
-
scenarioSync(0);
|
|
1424
|
-
// gates run starting in parallel
|
|
1425
|
-
const runId = genRunId();
|
|
1426
|
-
later(800, () => ingest({ type: "run.start", ts: Date.now(), runId, payload: { tool: "gates.run", target: "claude-code" } }));
|
|
1427
|
-
const stages = ["lint", "typecheck", "schema", "smoke"];
|
|
1428
|
-
stages.forEach((s, i) => {
|
|
1429
|
-
later(800 + (i + 1) * 1100, () => ingest({
|
|
1430
|
-
type: "progress", ts: Date.now(), runId,
|
|
1431
|
-
payload: { percent: Math.round(((i + 1) / stages.length) * 100), label: `gate: ${s}`, current: i + 1, total: stages.length, kind: "task" }
|
|
1432
|
-
}));
|
|
1433
|
-
});
|
|
1434
|
-
later(800 + stages.length * 1100 + 300, () => ingest({
|
|
1435
|
-
type: "run.end", ts: Date.now(), runId, payload: { tool: "gates.run", ok: true, duration_ms: stages.length * 1100 }
|
|
1436
|
-
}));
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
function scenarioError() {
|
|
1440
|
-
const runId = genRunId();
|
|
1441
|
-
later(0, () => ingest({ type: "run.start", ts: Date.now(), runId, payload: { tool: "reverse.scan", target: "cursor" } }));
|
|
1442
|
-
later(400, () => ingest({ type: "progress", ts: Date.now(), runId, payload: { percent: 18, label: "lendo .cursor/rules/", current: 2, total: 11, kind: "fs" } }));
|
|
1443
|
-
later(900, () => ingest({ type: "progress", ts: Date.now(), runId, payload: { percent: 38, label: "scanning .cursor/rules/architecture.mdc", current: 4, total: 11, kind: "fs" } }));
|
|
1444
|
-
later(1500, () => ingest({ type: "error", ts: Date.now(), runId, payload: { message: "ENOENT: file not found", code: "ENOENT" } }));
|
|
1445
|
-
later(1700, () => ingest({ type: "run.end", ts: Date.now(), runId, payload: { tool: "reverse.scan", ok: false, duration_ms: 1700 } }));
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
function scenarioIdle() {
|
|
1449
|
-
/* nothing */
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
let currentScenario = "sync";
|
|
1453
|
-
function runScenario(name) {
|
|
1454
|
-
currentScenario = name;
|
|
1455
|
-
if (name === "sync") scenarioSync();
|
|
1456
|
-
else if (name === "multi") scenarioMulti();
|
|
1457
|
-
else if (name === "error") scenarioError();
|
|
1458
|
-
else if (name === "idle") scenarioIdle();
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1728
|
+
/* ───────────────────── view actions ───────────────────── */
|
|
1729
|
+
/* "limpar tela" zera o que está VISÍVEL (eventos correntes + cards ativos),
|
|
1730
|
+
mas NUNCA toca o histórico de runs concluídas — esse é o ponto do drawer. */
|
|
1461
1731
|
function clearAll() {
|
|
1462
|
-
clearMock();
|
|
1463
1732
|
state.events = [];
|
|
1464
1733
|
state.runs.clear();
|
|
1465
1734
|
state._seenKeys = new Set();
|
|
1735
|
+
state.totalTokens = 0;
|
|
1466
1736
|
els.active.innerHTML = "";
|
|
1467
1737
|
render();
|
|
1468
1738
|
els.lastSeen.textContent = "aguardando…";
|
|
@@ -1498,14 +1768,30 @@ function wireSeg(rootSel, attr, fn) {
|
|
|
1498
1768
|
}
|
|
1499
1769
|
wireSeg("#tw-density", "v", (v) => document.documentElement.dataset.density = v);
|
|
1500
1770
|
wireSeg("#tw-motion", "v", (v) => document.documentElement.dataset.motion = v);
|
|
1501
|
-
wireSeg("#tw-scenario","v", (v) => { clearAll(); runScenario(v); });
|
|
1502
1771
|
|
|
1503
1772
|
document.documentElement.dataset.density = "normal";
|
|
1504
1773
|
document.documentElement.dataset.motion = "medium";
|
|
1505
1774
|
|
|
1506
|
-
document.getElementById("tw-replay").addEventListener("click", () => { clearAll(); runScenario(currentScenario); });
|
|
1507
1775
|
document.getElementById("tw-clear").addEventListener("click", clearAll);
|
|
1508
1776
|
|
|
1777
|
+
/* ───────────────────── history wiring ───────────────────── */
|
|
1778
|
+
els.historyBtn.addEventListener("click", () => {
|
|
1779
|
+
const open = els.history.classList.toggle("open");
|
|
1780
|
+
els.historyBtn.setAttribute("aria-pressed", open);
|
|
1781
|
+
if (open) renderHistory();
|
|
1782
|
+
});
|
|
1783
|
+
els.historyClose.addEventListener("click", () => {
|
|
1784
|
+
els.history.classList.remove("open");
|
|
1785
|
+
els.historyBtn.setAttribute("aria-pressed", "false");
|
|
1786
|
+
});
|
|
1787
|
+
// Esc fecha o drawer se ele estiver aberto e nada mais focado
|
|
1788
|
+
document.addEventListener("keydown", (e) => {
|
|
1789
|
+
if (e.key === "Escape" && els.history.classList.contains("open") && document.activeElement !== els.q) {
|
|
1790
|
+
els.history.classList.remove("open");
|
|
1791
|
+
els.historyBtn.setAttribute("aria-pressed", "false");
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1509
1795
|
/* ───────────────────── toolbar wiring ───────────────────── */
|
|
1510
1796
|
els.q.addEventListener("input", (e) => { state.filterText = e.target.value.trim(); render(); });
|
|
1511
1797
|
document.addEventListener("keydown", (e) => {
|
|
@@ -1564,6 +1850,12 @@ function applyConnState(s) {
|
|
|
1564
1850
|
els.conn.dataset.state = "on";
|
|
1565
1851
|
els.connLabel.textContent = "conectado";
|
|
1566
1852
|
if (els.srcLabel) els.srcLabel.textContent = "ao vivo";
|
|
1853
|
+
// Healthy => banner stale. Independente de qual caminho colocou ele lá
|
|
1854
|
+
// (shutdown event antigo, race, hydrate trazendo shutdown histórico),
|
|
1855
|
+
// estar conectado significa que o aviso "Reabra com kit ui start" não
|
|
1856
|
+
// serve mais — remova sempre.
|
|
1857
|
+
const banner = document.getElementById("shutdown-banner");
|
|
1858
|
+
if (banner) banner.remove();
|
|
1567
1859
|
} else if (s === "connecting") {
|
|
1568
1860
|
els.conn.dataset.state = "off";
|
|
1569
1861
|
els.connLabel.textContent = "conectando";
|
|
@@ -1611,11 +1903,16 @@ function connectRealSource() {
|
|
|
1611
1903
|
evtSource.addEventListener("open", () => {
|
|
1612
1904
|
lastConnectedAt = Date.now();
|
|
1613
1905
|
applyConnState("open");
|
|
1906
|
+
stopHealthPoll(); // já tá conectado, não precisa pollar
|
|
1614
1907
|
});
|
|
1615
1908
|
|
|
1616
1909
|
evtSource.addEventListener("error", () => {
|
|
1617
|
-
// EventSource auto-retries;
|
|
1910
|
+
// EventSource auto-retries; nós só atualizamos o estado visível e iniciamos
|
|
1911
|
+
// o poll-de-saúde paralelo, que cobre o caso "server reiniciou" — o native
|
|
1912
|
+
// retry às vezes gruda em CONNECTING sem nunca virar OPEN, e o poll detecta
|
|
1913
|
+
// quando o /healthz volta a responder e força um connectRealSource() limpo.
|
|
1618
1914
|
applyConnState("closed");
|
|
1915
|
+
startHealthPoll();
|
|
1619
1916
|
});
|
|
1620
1917
|
|
|
1621
1918
|
// Listen for each typed event the server emits.
|
|
@@ -1625,6 +1922,9 @@ function connectRealSource() {
|
|
|
1625
1922
|
if (data && data.type === "shutdown") {
|
|
1626
1923
|
applyConnState("shutdown");
|
|
1627
1924
|
showShutdownBanner();
|
|
1925
|
+
// Mesmo após shutdown, um novo `kit ui start` faz o server voltar.
|
|
1926
|
+
// Continue tentando reconectar — usuário não precisa recarregar a aba.
|
|
1927
|
+
startHealthPoll();
|
|
1628
1928
|
}
|
|
1629
1929
|
ingest(data);
|
|
1630
1930
|
} catch (_) { /* swallow malformed */ }
|
|
@@ -1635,12 +1935,41 @@ function connectRealSource() {
|
|
|
1635
1935
|
evtSource.onmessage = handler; // fallback for unnamed
|
|
1636
1936
|
}
|
|
1637
1937
|
|
|
1638
|
-
/*
|
|
1639
|
-
|
|
1640
|
-
|
|
1938
|
+
/* ───────────────────── health-poll auto-reconnect ─────────────────────
|
|
1939
|
+
Quando o EventSource cai e fica preso (server reiniciado, sidecar `kit ui
|
|
1940
|
+
stop` seguido de `kit ui start`, network blip), o native retry pode estagnar
|
|
1941
|
+
em CONNECTING. Esse poll roda em segundo plano em paralelo: quando o /healthz
|
|
1942
|
+
volta a responder 200, fechamos o EventSource antigo, hidratamos /state, e
|
|
1943
|
+
abrimos um novo. Para de pollar assim que conectamos com sucesso. */
|
|
1944
|
+
let healthPollTimer = null;
|
|
1945
|
+
function startHealthPoll() {
|
|
1946
|
+
if (healthPollTimer) return;
|
|
1947
|
+
healthPollTimer = setInterval(async () => {
|
|
1948
|
+
if (els.conn.dataset.state === "on") { stopHealthPoll(); return; }
|
|
1949
|
+
try {
|
|
1950
|
+
const res = await fetch("/healthz", { credentials: "omit", cache: "no-store" });
|
|
1951
|
+
if (res.ok) {
|
|
1952
|
+
stopHealthPoll();
|
|
1953
|
+
// Banner de shutdown é stale agora que o server voltou — remova.
|
|
1954
|
+
const banner = document.getElementById("shutdown-banner");
|
|
1955
|
+
if (banner) banner.remove();
|
|
1956
|
+
await hydrateFromState();
|
|
1957
|
+
connectRealSource();
|
|
1958
|
+
}
|
|
1959
|
+
} catch (_) { /* server ainda não voltou — keep tentando */ }
|
|
1960
|
+
}, 3000);
|
|
1961
|
+
}
|
|
1962
|
+
function stopHealthPoll() {
|
|
1963
|
+
if (healthPollTimer) { clearInterval(healthPollTimer); healthPollTimer = null; }
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/* Background-tab recovery — quando o tab volta a ficar visível e estamos fora
|
|
1967
|
+
do estado conectado, força um reconnect imediato em vez de esperar o próximo
|
|
1968
|
+
tick de health poll. */
|
|
1641
1969
|
document.addEventListener("visibilitychange", () => {
|
|
1642
1970
|
if (document.visibilityState !== "visible") return;
|
|
1643
|
-
if (els.conn.dataset.state
|
|
1971
|
+
if (els.conn.dataset.state !== "on") {
|
|
1972
|
+
stopHealthPoll();
|
|
1644
1973
|
hydrateFromState().then(connectRealSource);
|
|
1645
1974
|
}
|
|
1646
1975
|
});
|
|
@@ -1671,22 +2000,14 @@ function showShutdownBanner() {
|
|
|
1671
2000
|
}
|
|
1672
2001
|
|
|
1673
2002
|
/* ───────────────────── boot ───────────────────── */
|
|
1674
|
-
/*
|
|
1675
|
-
source of truth. The mock scenarios are still wired and accessible through
|
|
1676
|
-
the tweaks panel for demo/dev. */
|
|
2003
|
+
/* `/events` SSE é a única fonte de verdade. Nenhum mock no boot. */
|
|
1677
2004
|
|
|
2005
|
+
loadHistory(); // restaura o histórico desta sessão (sessionStorage)
|
|
1678
2006
|
(async () => {
|
|
1679
2007
|
await hydrateFromState();
|
|
1680
2008
|
connectRealSource();
|
|
2009
|
+
render(); // pinta footer + drawer mesmo antes do primeiro evento real
|
|
1681
2010
|
})();
|
|
1682
|
-
|
|
1683
|
-
// Detect "we're being served via http(s)" — when not, we're probably the
|
|
1684
|
-
// design preview opened via file://; in that case fall back to the mock.
|
|
1685
|
-
if (location.protocol === "file:") {
|
|
1686
|
-
setTimeout(() => {
|
|
1687
|
-
if (state.events.length === 0) runScenario("sync");
|
|
1688
|
-
}, 200);
|
|
1689
|
-
}
|
|
1690
2011
|
</script>
|
|
1691
2012
|
</body>
|
|
1692
2013
|
</html>
|