@luanpdd/kit-mcp 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/package.json +1 -1
- package/src/ui/static/index.html +413 -139
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,39 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.5.0] - 2026-05-05
|
|
10
|
+
|
|
11
|
+
UI sidecar — bug fixes visuais + tokens + histórico de sessão.
|
|
12
|
+
|
|
13
|
+
### Adicionado
|
|
14
|
+
|
|
15
|
+
- **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`.
|
|
16
|
+
- **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.
|
|
17
|
+
- **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).
|
|
18
|
+
- **Footer mostra runs concluídas** total da sessão (`3 runs concluídas`).
|
|
19
|
+
|
|
20
|
+
### Corrigido
|
|
21
|
+
|
|
22
|
+
- **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.
|
|
23
|
+
- **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.
|
|
24
|
+
- **Active card sem título** — antes mostrava `—` sozinho se `payload.tool` estava vazio. Helper `runTitle(run)` cascata `humanizeTool(tool) → lastTitle → lastLabel → lastName → "Processo"`.
|
|
25
|
+
- **Tool inline mostrava `—` em vez de fallback** — `escapeHtml(safeStr(run.tool) || "processo")` no rc-tool, `escapeHtml(safeStr(run.tool) || "")` no rc-foot.
|
|
26
|
+
|
|
27
|
+
### Removido
|
|
28
|
+
|
|
29
|
+
- **Painel "Cenário (mock)" do Tweaks** — apenas cenários reais agora; sem botões de demo.
|
|
30
|
+
- **Botão "▸ replay"** — dependia de mock.
|
|
31
|
+
- **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.
|
|
32
|
+
- **Fallback `file://` boot** — sidecar não é mais aberto via `file://`; só via servidor.
|
|
33
|
+
|
|
34
|
+
### Sem mudanças de API runtime
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
### Migration
|
|
39
|
+
|
|
40
|
+
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).
|
|
41
|
+
|
|
9
42
|
## [1.4.0] - 2026-05-05
|
|
10
43
|
|
|
11
44
|
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.0",
|
|
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
|
@@ -802,6 +802,154 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
802
802
|
}
|
|
803
803
|
.tw-actions button:hover { color: var(--text); background: var(--surface-3); }
|
|
804
804
|
|
|
805
|
+
/* ───────────── tokens chips ───────────── */
|
|
806
|
+
.tokens-chip {
|
|
807
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
808
|
+
font-family: var(--mono);
|
|
809
|
+
font-size: 11px;
|
|
810
|
+
color: var(--text-2);
|
|
811
|
+
padding: 1px 6px;
|
|
812
|
+
border: 1px solid var(--line);
|
|
813
|
+
border-radius: 999px;
|
|
814
|
+
background: var(--surface-2);
|
|
815
|
+
font-variant-numeric: tabular-nums;
|
|
816
|
+
white-space: nowrap;
|
|
817
|
+
}
|
|
818
|
+
.tokens-chip::before {
|
|
819
|
+
content: "⌬";
|
|
820
|
+
color: var(--text-3);
|
|
821
|
+
font-size: 10px;
|
|
822
|
+
}
|
|
823
|
+
.rc-tokens {
|
|
824
|
+
margin-top: 6px;
|
|
825
|
+
font-family: var(--mono);
|
|
826
|
+
color: var(--text-3);
|
|
827
|
+
font-size: 11px;
|
|
828
|
+
display: flex; gap: 14px;
|
|
829
|
+
align-items: center;
|
|
830
|
+
}
|
|
831
|
+
.rc-tokens .num { color: var(--text-2); font-variant-numeric: tabular-nums; }
|
|
832
|
+
|
|
833
|
+
/* ───────────── history drawer ───────────── */
|
|
834
|
+
.history {
|
|
835
|
+
position: fixed;
|
|
836
|
+
right: 16px; bottom: 16px;
|
|
837
|
+
width: 360px;
|
|
838
|
+
max-height: 70vh;
|
|
839
|
+
background: var(--surface-2);
|
|
840
|
+
border: 1px solid var(--line-strong);
|
|
841
|
+
border-radius: 12px;
|
|
842
|
+
box-shadow: 0 24px 60px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.02);
|
|
843
|
+
z-index: 50;
|
|
844
|
+
overflow: hidden;
|
|
845
|
+
display: none;
|
|
846
|
+
flex-direction: column;
|
|
847
|
+
animation: tweaks-in .25s var(--ease);
|
|
848
|
+
}
|
|
849
|
+
.history.open { display: flex; }
|
|
850
|
+
.history h3 {
|
|
851
|
+
margin: 0;
|
|
852
|
+
padding: 12px 14px;
|
|
853
|
+
font-family: var(--mono);
|
|
854
|
+
font-size: 10px;
|
|
855
|
+
text-transform: uppercase;
|
|
856
|
+
letter-spacing: .14em;
|
|
857
|
+
color: var(--text-3);
|
|
858
|
+
display: flex; align-items: center; gap: 8px;
|
|
859
|
+
border-bottom: 1px solid var(--line);
|
|
860
|
+
flex-shrink: 0;
|
|
861
|
+
}
|
|
862
|
+
.history h3 .hist-meta {
|
|
863
|
+
color: var(--text-2);
|
|
864
|
+
font-size: 10px;
|
|
865
|
+
font-weight: 400;
|
|
866
|
+
text-transform: none;
|
|
867
|
+
letter-spacing: .04em;
|
|
868
|
+
margin-left: auto;
|
|
869
|
+
}
|
|
870
|
+
.history h3 .close {
|
|
871
|
+
width: 18px; height: 18px;
|
|
872
|
+
display: grid; place-items: center;
|
|
873
|
+
border-radius: 4px;
|
|
874
|
+
color: var(--text-3);
|
|
875
|
+
}
|
|
876
|
+
.history h3 .close:hover { background: var(--surface-3); color: var(--text); }
|
|
877
|
+
.history-body { padding: 8px; overflow-y: auto; flex: 1; }
|
|
878
|
+
.hist-empty {
|
|
879
|
+
padding: 24px 12px;
|
|
880
|
+
text-align: center;
|
|
881
|
+
color: var(--text-3);
|
|
882
|
+
font-size: 12px;
|
|
883
|
+
}
|
|
884
|
+
.hist-row {
|
|
885
|
+
display: grid;
|
|
886
|
+
grid-template-columns: 22px 1fr auto;
|
|
887
|
+
gap: 4px 10px;
|
|
888
|
+
padding: 10px;
|
|
889
|
+
border: 1px solid var(--line);
|
|
890
|
+
border-radius: 8px;
|
|
891
|
+
margin-bottom: 6px;
|
|
892
|
+
background: var(--surface-1);
|
|
893
|
+
transition: border-color .15s var(--ease);
|
|
894
|
+
cursor: pointer;
|
|
895
|
+
}
|
|
896
|
+
.hist-row:hover { border-color: var(--line-strong); background: var(--surface-3); }
|
|
897
|
+
.hist-row[data-status="error"] { border-left: 3px solid var(--err); }
|
|
898
|
+
.hist-row[data-status="ok"] { border-left: 3px solid var(--accent); }
|
|
899
|
+
.hist-status {
|
|
900
|
+
width: 18px; height: 18px;
|
|
901
|
+
display: grid; place-items: center;
|
|
902
|
+
border-radius: 999px;
|
|
903
|
+
font-family: var(--mono);
|
|
904
|
+
font-size: 10px;
|
|
905
|
+
font-weight: 700;
|
|
906
|
+
grid-row: 1 / span 1;
|
|
907
|
+
}
|
|
908
|
+
.hist-row[data-status="ok"] .hist-status { color: var(--accent); background: var(--accent-soft); }
|
|
909
|
+
.hist-row[data-status="error"] .hist-status { color: var(--err); background: var(--err-soft); }
|
|
910
|
+
.hist-row[data-status="running"] .hist-status { color: var(--text-2); background: var(--surface-3); }
|
|
911
|
+
.hist-title {
|
|
912
|
+
font-size: 13px;
|
|
913
|
+
color: var(--text);
|
|
914
|
+
font-weight: 500;
|
|
915
|
+
min-width: 0;
|
|
916
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
917
|
+
}
|
|
918
|
+
.hist-when {
|
|
919
|
+
font-family: var(--mono);
|
|
920
|
+
font-size: 10px;
|
|
921
|
+
color: var(--text-3);
|
|
922
|
+
text-align: right;
|
|
923
|
+
grid-row: 1 / span 1;
|
|
924
|
+
}
|
|
925
|
+
.hist-meta-row {
|
|
926
|
+
grid-column: 2 / span 2;
|
|
927
|
+
display: flex; gap: 12px; flex-wrap: wrap;
|
|
928
|
+
font-family: var(--mono);
|
|
929
|
+
font-size: 10px;
|
|
930
|
+
color: var(--text-3);
|
|
931
|
+
}
|
|
932
|
+
.hist-meta-row .num { color: var(--text-2); font-variant-numeric: tabular-nums; }
|
|
933
|
+
.hist-detail {
|
|
934
|
+
grid-column: 1 / -1;
|
|
935
|
+
margin-top: 6px;
|
|
936
|
+
padding: 8px;
|
|
937
|
+
border: 1px solid var(--line);
|
|
938
|
+
border-radius: 6px;
|
|
939
|
+
background: var(--surface-2);
|
|
940
|
+
font-family: var(--mono);
|
|
941
|
+
font-size: 11px;
|
|
942
|
+
color: var(--text-2);
|
|
943
|
+
max-height: 240px;
|
|
944
|
+
overflow-y: auto;
|
|
945
|
+
display: none;
|
|
946
|
+
}
|
|
947
|
+
.hist-row.expanded .hist-detail { display: block; }
|
|
948
|
+
.hist-detail-row { padding: 2px 0; display: flex; gap: 6px; }
|
|
949
|
+
.hist-detail-row .pct { color: var(--accent); flex-shrink: 0; min-width: 36px; font-variant-numeric: tabular-nums; }
|
|
950
|
+
.hist-detail-row .lbl { color: var(--text-2); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
951
|
+
.hist-detail-row .tok { color: var(--text-3); flex-shrink: 0; }
|
|
952
|
+
|
|
805
953
|
/* density-driven log padding handled via --pad-tight above */
|
|
806
954
|
|
|
807
955
|
/* ───────────────────────── reduced motion ───────────────────────── */
|
|
@@ -868,6 +1016,11 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
868
1016
|
<rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/>
|
|
869
1017
|
</svg>
|
|
870
1018
|
</button>
|
|
1019
|
+
<button class="iconbtn" id="history-btn" aria-pressed="false" aria-label="Histórico desta sessão" title="Histórico desta sessão">
|
|
1020
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1021
|
+
<path d="M3 12a9 9 0 1 0 3-7"/><path d="M3 4v5h5"/><path d="M12 7v5l3 2"/>
|
|
1022
|
+
</svg>
|
|
1023
|
+
</button>
|
|
871
1024
|
<button class="iconbtn" id="tweaks-btn" aria-pressed="false" aria-label="Tweaks" title="Tweaks">
|
|
872
1025
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
873
1026
|
<circle cx="12" cy="12" r="3"/>
|
|
@@ -910,6 +1063,10 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
910
1063
|
<span>fonte: <span class="live" id="src-label">ao vivo</span></span>
|
|
911
1064
|
<span class="sep">·</span>
|
|
912
1065
|
<span id="last-seen">aguardando…</span>
|
|
1066
|
+
<span class="sep" id="footer-tokens-sep" hidden>·</span>
|
|
1067
|
+
<span id="footer-tokens" hidden>0 tokens</span>
|
|
1068
|
+
<span class="sep">·</span>
|
|
1069
|
+
<span id="footer-runs">0 runs concluídas</span>
|
|
913
1070
|
</footer>
|
|
914
1071
|
|
|
915
1072
|
</div>
|
|
@@ -953,23 +1110,26 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
953
1110
|
</div>
|
|
954
1111
|
</div>
|
|
955
1112
|
|
|
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
1113
|
<div class="tw-actions">
|
|
967
|
-
<button id="tw-
|
|
968
|
-
<button id="tw-clear">limpar</button>
|
|
1114
|
+
<button id="tw-clear">limpar tela</button>
|
|
969
1115
|
</div>
|
|
970
1116
|
</div>
|
|
971
1117
|
</aside>
|
|
972
1118
|
|
|
1119
|
+
<!-- HISTORY DRAWER -->
|
|
1120
|
+
<aside class="history" id="history" role="dialog" aria-label="Histórico da sessão">
|
|
1121
|
+
<h3>
|
|
1122
|
+
Histórico desta sessão
|
|
1123
|
+
<span class="hist-meta" id="hist-meta">0 runs</span>
|
|
1124
|
+
<button class="close" id="history-close" aria-label="Fechar">
|
|
1125
|
+
<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>
|
|
1126
|
+
</button>
|
|
1127
|
+
</h3>
|
|
1128
|
+
<div class="history-body" id="history-body">
|
|
1129
|
+
<div class="hist-empty">Nada por aqui ainda. Quando uma run terminar, ela aparece aqui.</div>
|
|
1130
|
+
</div>
|
|
1131
|
+
</aside>
|
|
1132
|
+
|
|
973
1133
|
<!-- SVG sprite (in-doc, no external) -->
|
|
974
1134
|
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
975
1135
|
<defs>
|
|
@@ -1057,11 +1217,13 @@ function clockTime(ts) {
|
|
|
1057
1217
|
/* ---------- state ---------- */
|
|
1058
1218
|
const state = {
|
|
1059
1219
|
events: [], // newest first
|
|
1060
|
-
runs: new Map(), // runId → { startTs, tool, lastProgress, lastLabel, ended, ok, current, total }
|
|
1220
|
+
runs: new Map(), // runId → { startTs, tool, lastProgress, lastLabel, ended, ok, current, total, tokens, endTs }
|
|
1061
1221
|
filterText: "",
|
|
1062
1222
|
// tool_invocation + shutdown não têm chip de filtro, mas devem aparecer por default
|
|
1063
1223
|
filters: new Set(["run.start","progress","milestone","run.end","error","tool_invocation","shutdown"]),
|
|
1064
1224
|
paused: false,
|
|
1225
|
+
totalTokens: 0, // soma de payload.tokens em TODOS os eventos da sessão
|
|
1226
|
+
history: [], // runs já concluídas neste tab — persistido em sessionStorage
|
|
1065
1227
|
};
|
|
1066
1228
|
|
|
1067
1229
|
/* ---------- DOM refs ---------- */
|
|
@@ -1085,6 +1247,14 @@ const els = {
|
|
|
1085
1247
|
tweaks: $("#tweaks"),
|
|
1086
1248
|
tweaksClose: $("#tweaks-close"),
|
|
1087
1249
|
srcLabel: $("#src-label"),
|
|
1250
|
+
historyBtn: $("#history-btn"),
|
|
1251
|
+
history: $("#history"),
|
|
1252
|
+
historyClose: $("#history-close"),
|
|
1253
|
+
historyBody: $("#history-body"),
|
|
1254
|
+
histMeta: $("#hist-meta"),
|
|
1255
|
+
footerTokens: $("#footer-tokens"),
|
|
1256
|
+
footerTokensSep: $("#footer-tokens-sep"),
|
|
1257
|
+
footerRuns: $("#footer-runs"),
|
|
1088
1258
|
};
|
|
1089
1259
|
|
|
1090
1260
|
/* ---------- ingest one event ---------- */
|
|
@@ -1093,16 +1263,24 @@ function ingest(evt) {
|
|
|
1093
1263
|
state.events.unshift(evt);
|
|
1094
1264
|
if (state.events.length > 200) state.events.length = 200;
|
|
1095
1265
|
|
|
1266
|
+
// tokens — soma global da sessão + por run
|
|
1267
|
+
const tk = readTokens(evt);
|
|
1268
|
+
if (tk > 0) state.totalTokens += tk;
|
|
1269
|
+
|
|
1096
1270
|
// run tracking
|
|
1097
1271
|
if (evt.runId) {
|
|
1098
1272
|
let run = state.runs.get(evt.runId);
|
|
1099
1273
|
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 };
|
|
1274
|
+
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
1275
|
state.runs.set(evt.runId, run);
|
|
1102
1276
|
}
|
|
1103
1277
|
run.lastTs = evt.ts;
|
|
1278
|
+
run.tokens = (run.tokens || 0) + tk;
|
|
1279
|
+
// store a slimmed-down record of every event in the run so the history
|
|
1280
|
+
// drawer can show what happened later
|
|
1281
|
+
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
1282
|
if (evt.type === "run.start") {
|
|
1105
|
-
run.tool = evt.payload?.tool;
|
|
1283
|
+
run.tool = evt.payload?.tool || run.tool;
|
|
1106
1284
|
run.startTs = evt.ts;
|
|
1107
1285
|
} else if (evt.type === "progress") {
|
|
1108
1286
|
if (typeof evt.payload?.percent === "number") run.lastProgress = evt.payload.percent;
|
|
@@ -1112,9 +1290,13 @@ function ingest(evt) {
|
|
|
1112
1290
|
} else if (evt.type === "run.end") {
|
|
1113
1291
|
run.ended = true;
|
|
1114
1292
|
run.ok = !!evt.payload?.ok;
|
|
1293
|
+
run.endTs = evt.ts;
|
|
1115
1294
|
run.lastProgress = run.ok ? 100 : run.lastProgress;
|
|
1295
|
+
// arquive into history (client-only, sessionStorage)
|
|
1296
|
+
archiveRun(run);
|
|
1116
1297
|
} else if (evt.type === "error") {
|
|
1117
|
-
//
|
|
1298
|
+
// não termina o run; só registra
|
|
1299
|
+
run.lastError = evt.payload?.message || "erro";
|
|
1118
1300
|
}
|
|
1119
1301
|
}
|
|
1120
1302
|
|
|
@@ -1122,6 +1304,55 @@ function ingest(evt) {
|
|
|
1122
1304
|
els.lastSeen.textContent = "último: " + clockTime(evt.ts);
|
|
1123
1305
|
}
|
|
1124
1306
|
|
|
1307
|
+
/* ---------- helpers — tokens & history ---------- */
|
|
1308
|
+
function readTokens(evt) {
|
|
1309
|
+
// Aceita várias chaves comuns: payload.tokens (preferido), payload.usage.total_tokens, payload.cost.tokens
|
|
1310
|
+
const p = evt.payload || {};
|
|
1311
|
+
if (typeof p.tokens === "number") return p.tokens;
|
|
1312
|
+
if (p.usage && typeof p.usage.total_tokens === "number") return p.usage.total_tokens;
|
|
1313
|
+
if (p.cost && typeof p.cost.tokens === "number") return p.cost.tokens;
|
|
1314
|
+
return 0;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function fmtTokens(n) {
|
|
1318
|
+
if (n < 1000) return String(n);
|
|
1319
|
+
if (n < 10_000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k";
|
|
1320
|
+
if (n < 1_000_000) return Math.round(n / 1000) + "k";
|
|
1321
|
+
return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function archiveRun(run) {
|
|
1325
|
+
// Snapshot leve da run completa — o que vai pra sessionStorage tem que ser serializável.
|
|
1326
|
+
const entry = {
|
|
1327
|
+
runId: run.runId,
|
|
1328
|
+
tool: run.tool,
|
|
1329
|
+
startTs: run.startTs,
|
|
1330
|
+
endTs: run.endTs,
|
|
1331
|
+
durationMs: (run.endTs || Date.now()) - run.startTs,
|
|
1332
|
+
ok: run.ok,
|
|
1333
|
+
tokens: run.tokens || 0,
|
|
1334
|
+
eventsCount: run.events.length,
|
|
1335
|
+
events: run.events.slice(-100), // últimos 100 eventos da run pra detalhe
|
|
1336
|
+
};
|
|
1337
|
+
state.history.unshift(entry);
|
|
1338
|
+
// cap em 50 runs pra não estourar sessionStorage
|
|
1339
|
+
if (state.history.length > 50) state.history.length = 50;
|
|
1340
|
+
saveHistory();
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function saveHistory() {
|
|
1344
|
+
try {
|
|
1345
|
+
sessionStorage.setItem("kit-mcp-history", JSON.stringify(state.history));
|
|
1346
|
+
} catch (_) { /* quota? sem espaço? sem stress. */ }
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function loadHistory() {
|
|
1350
|
+
try {
|
|
1351
|
+
const raw = sessionStorage.getItem("kit-mcp-history");
|
|
1352
|
+
if (raw) state.history = JSON.parse(raw) || [];
|
|
1353
|
+
} catch (_) { state.history = []; }
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1125
1356
|
/* ---------- render ---------- */
|
|
1126
1357
|
function passesFilter(evt) {
|
|
1127
1358
|
if (!state.filters.has(evt.type)) return false;
|
|
@@ -1137,6 +1368,64 @@ function render() {
|
|
|
1137
1368
|
const total = state.events.length;
|
|
1138
1369
|
els.evtCount.textContent = `${total} evento${total === 1 ? "" : "s"}`;
|
|
1139
1370
|
els.logCount.textContent = `${total} evento${total === 1 ? "" : "s"}`;
|
|
1371
|
+
// Tokens (mostra só quando algum evento da sessão veio com payload.tokens)
|
|
1372
|
+
if (state.totalTokens > 0) {
|
|
1373
|
+
els.footerTokens.hidden = false;
|
|
1374
|
+
els.footerTokensSep.hidden = false;
|
|
1375
|
+
els.footerTokens.textContent = `${fmtTokens(state.totalTokens)} tokens nesta sessão`;
|
|
1376
|
+
} else {
|
|
1377
|
+
els.footerTokens.hidden = true;
|
|
1378
|
+
els.footerTokensSep.hidden = true;
|
|
1379
|
+
}
|
|
1380
|
+
const completed = state.history.length;
|
|
1381
|
+
els.footerRuns.textContent = `${completed} run${completed === 1 ? "" : "s"} concluída${completed === 1 ? "" : "s"}`;
|
|
1382
|
+
if (els.histMeta) els.histMeta.textContent = `${completed} run${completed === 1 ? "" : "s"}`;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/* ---------- history drawer renderer ---------- */
|
|
1386
|
+
function renderHistory() {
|
|
1387
|
+
if (!els.historyBody) return;
|
|
1388
|
+
if (state.history.length === 0) {
|
|
1389
|
+
els.historyBody.innerHTML = `<div class="hist-empty">Nada por aqui ainda. Quando uma run terminar, ela aparece aqui.</div>`;
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
const rows = state.history.map((h) => historyRowHtml(h)).join("");
|
|
1393
|
+
els.historyBody.innerHTML = rows;
|
|
1394
|
+
els.historyBody.querySelectorAll(".hist-row").forEach((row) => {
|
|
1395
|
+
row.addEventListener("click", () => row.classList.toggle("expanded"));
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function historyRowHtml(h) {
|
|
1400
|
+
const status = h.ok === true ? "ok" : h.ok === false ? "error" : "running";
|
|
1401
|
+
const statusGlyph = status === "ok" ? "✓" : status === "error" ? "✕" : "·";
|
|
1402
|
+
const title = humanizeTool(safeStr(h.tool || "")) || safeStr(h.tool) || "Processo";
|
|
1403
|
+
const dur = formatElapsed(h.durationMs || 0);
|
|
1404
|
+
const when = clockTime(h.startTs);
|
|
1405
|
+
const tokens = h.tokens > 0 ? `<span class="num">${fmtTokens(h.tokens)}</span> tokens` : "<span>sem tokens registrados</span>";
|
|
1406
|
+
const detailRows = (h.events || [])
|
|
1407
|
+
.filter((e) => e.type === "progress" || e.type === "milestone" || e.type === "error" || e.type === "run.end")
|
|
1408
|
+
.map((e) => {
|
|
1409
|
+
const pct = typeof e.percent === "number" ? `${Math.round(e.percent)}%` : "·";
|
|
1410
|
+
const lbl = safeStr(e.label) || safeStr(e.name) || safeStr(e.message) || humanizeEventType(e.type);
|
|
1411
|
+
const tk = e.tokens > 0 ? fmtTokens(e.tokens) : "";
|
|
1412
|
+
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>`;
|
|
1413
|
+
})
|
|
1414
|
+
.join("");
|
|
1415
|
+
return `
|
|
1416
|
+
<div class="hist-row" data-status="${status}" data-runid="${h.runId}">
|
|
1417
|
+
<div class="hist-status">${statusGlyph}</div>
|
|
1418
|
+
<div class="hist-title">${escapeHtml(title)}</div>
|
|
1419
|
+
<div class="hist-when">${when}</div>
|
|
1420
|
+
<div class="hist-meta-row">
|
|
1421
|
+
<span><span class="num">${dur}</span> dur</span>
|
|
1422
|
+
<span>${tokens}</span>
|
|
1423
|
+
<span><span class="num">${h.eventsCount || 0}</span> eventos</span>
|
|
1424
|
+
<span class="num">id ${h.runId.slice(0,8)}</span>
|
|
1425
|
+
</div>
|
|
1426
|
+
<div class="hist-detail">${detailRows || '<div class="hist-detail-row"><span class="lbl">(sem progresso registrado)</span></div>'}</div>
|
|
1427
|
+
</div>
|
|
1428
|
+
`;
|
|
1140
1429
|
}
|
|
1141
1430
|
|
|
1142
1431
|
function renderActive() {
|
|
@@ -1165,18 +1454,44 @@ function renderActive() {
|
|
|
1165
1454
|
});
|
|
1166
1455
|
}
|
|
1167
1456
|
|
|
1457
|
+
/**
|
|
1458
|
+
* Sanitização: remove o "U+FFFD replacement character" e variantes que
|
|
1459
|
+
* indicam mojibake — eventos vindos de publishers em locale ruim podem
|
|
1460
|
+
* vazar bytes corrompidos. Em vez de mostrar `�`, devolvemos string vazia
|
|
1461
|
+
* e deixamos o fallback escolher um nome decente.
|
|
1462
|
+
*/
|
|
1463
|
+
function safeStr(s) {
|
|
1464
|
+
if (typeof s !== "string") return "";
|
|
1465
|
+
// remove FFFD (U+FFFD) e null bytes
|
|
1466
|
+
const cleaned = s.replace(/�+/g, "").replace(/+/g, "").trim();
|
|
1467
|
+
return cleaned;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Resolve o melhor título legível para uma run, NUNCA devolve "—" sozinho.
|
|
1472
|
+
* Cascata: humanizeTool(payload.tool) → payload.title → payload.label →
|
|
1473
|
+
* payload.name → "Processo".
|
|
1474
|
+
*/
|
|
1475
|
+
function runTitle(run) {
|
|
1476
|
+
const t = humanizeTool(safeStr(run.tool || ""));
|
|
1477
|
+
if (t && t !== "—") return t;
|
|
1478
|
+
const fallback = safeStr(run.lastTitle) || safeStr(run.lastLabel) || safeStr(run.lastName);
|
|
1479
|
+
if (fallback) return fallback;
|
|
1480
|
+
return "Processo";
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1168
1483
|
function updateActiveCard(card, run) {
|
|
1169
1484
|
const family = TOOL_FAMILIES[run.tool] || "sync";
|
|
1170
1485
|
const percent = Math.max(0, Math.min(100, Math.round(run.lastProgress)));
|
|
1171
1486
|
const elapsed = Date.now() - run.startTs;
|
|
1172
1487
|
const longRunning = elapsed > 30_000;
|
|
1173
|
-
const stepLabel = humanizePath(run.lastLabel) || "iniciando…";
|
|
1488
|
+
const stepLabel = humanizePath(safeStr(run.lastLabel)) || "iniciando…";
|
|
1174
1489
|
const stepCount = run.total ? `${run.current}/${run.total}` : "";
|
|
1175
1490
|
|
|
1176
1491
|
const set = (sel, fn) => { const n = card.querySelector(sel); if (n) fn(n); };
|
|
1177
1492
|
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 =
|
|
1493
|
+
set(".rc-tool", (n) => { n.textContent = safeStr(run.tool) || "processo"; });
|
|
1494
|
+
set(".rc-title", (n) => { n.textContent = runTitle(run); });
|
|
1180
1495
|
set(".rc-bar-fill", (n) => { n.style.width = percent + "%"; });
|
|
1181
1496
|
set(".rc-pct", (n) => { n.textContent = percent + "%"; });
|
|
1182
1497
|
set(".rc-step-text", (n) => { if (n.textContent !== stepLabel) n.textContent = stepLabel; });
|
|
@@ -1184,24 +1499,27 @@ function updateActiveCard(card, run) {
|
|
|
1184
1499
|
set(".rc-elapsed-val", (n) => { n.textContent = formatElapsed(elapsed); });
|
|
1185
1500
|
set(".rc-elapsed .em", (n) => n.classList.toggle("warn", longRunning));
|
|
1186
1501
|
set(".rc-runid", (n) => { n.textContent = "id " + run.runId.slice(0, 8); });
|
|
1502
|
+
set(".rc-tokens-val", (n) => { n.textContent = run.tokens > 0 ? fmtTokens(run.tokens) : "—"; });
|
|
1503
|
+
set(".rc-tokens", (n) => { n.classList.toggle("hidden", !(run.tokens > 0)); });
|
|
1187
1504
|
}
|
|
1188
1505
|
|
|
1189
1506
|
function activeCardHtml(run) {
|
|
1190
1507
|
const family = TOOL_FAMILIES[run.tool] || "sync";
|
|
1191
1508
|
const iconHref = `#i-${family}`;
|
|
1192
|
-
const title =
|
|
1193
|
-
const stepLabel = humanizePath(run.lastLabel) || "iniciando…";
|
|
1509
|
+
const title = runTitle(run);
|
|
1510
|
+
const stepLabel = humanizePath(safeStr(run.lastLabel)) || "iniciando…";
|
|
1194
1511
|
const percent = Math.max(0, Math.min(100, Math.round(run.lastProgress)));
|
|
1195
1512
|
const elapsed = Date.now() - run.startTs;
|
|
1196
1513
|
const longRunning = elapsed > 30_000;
|
|
1197
1514
|
const stepCount = run.total ? `${run.current}/${run.total}` : "";
|
|
1515
|
+
const showTokens = run.tokens > 0;
|
|
1198
1516
|
return `
|
|
1199
1517
|
<article class="run-card" data-runid="${run.runId}">
|
|
1200
1518
|
<div class="rc-head">
|
|
1201
1519
|
<div class="rc-icon" data-tool="${family}"><svg><use href="${iconHref}"/></svg></div>
|
|
1202
1520
|
<div class="rc-title-block">
|
|
1203
|
-
<div class="rc-tool">${run.tool || "
|
|
1204
|
-
<div class="rc-title">${title}</div>
|
|
1521
|
+
<div class="rc-tool">${escapeHtml(safeStr(run.tool) || "processo")}</div>
|
|
1522
|
+
<div class="rc-title">${escapeHtml(title)}</div>
|
|
1205
1523
|
</div>
|
|
1206
1524
|
<div class="rc-elapsed">
|
|
1207
1525
|
<span class="em ${longRunning ? "warn" : ""}"><span class="rc-elapsed-val">${formatElapsed(elapsed)}</span></span>
|
|
@@ -1220,10 +1538,15 @@ function activeCardHtml(run) {
|
|
|
1220
1538
|
${stepCount ? `<span class="rc-step-count">${stepCount}</span>` : ""}
|
|
1221
1539
|
</div>
|
|
1222
1540
|
|
|
1541
|
+
<div class="rc-tokens" ${showTokens ? "" : "style=\"display:none\""}>
|
|
1542
|
+
<span class="tokens-chip"><span class="rc-tokens-val">${showTokens ? fmtTokens(run.tokens) : "—"}</span></span>
|
|
1543
|
+
<span>tokens nesta run</span>
|
|
1544
|
+
</div>
|
|
1545
|
+
|
|
1223
1546
|
<div class="rc-foot">
|
|
1224
1547
|
<span class="rc-runid">id ${run.runId.slice(0, 8)}</span>
|
|
1225
1548
|
<span class="sep">·</span>
|
|
1226
|
-
<span>${run.tool || ""}</span>
|
|
1549
|
+
<span>${escapeHtml(safeStr(run.tool) || "")}</span>
|
|
1227
1550
|
</div>
|
|
1228
1551
|
</article>
|
|
1229
1552
|
`;
|
|
@@ -1296,39 +1619,71 @@ function rowHtml(evt, idx, prev) {
|
|
|
1296
1619
|
const time = clockTime(evt.ts);
|
|
1297
1620
|
const rel = relTime(Date.now() - evt.ts);
|
|
1298
1621
|
const badge = humanizeEventType(evt.type);
|
|
1299
|
-
const
|
|
1622
|
+
const tk = readTokens(evt);
|
|
1623
|
+
const tokenChip = tk > 0 ? ` <span class="tokens-chip" title="tokens">${fmtTokens(tk)}</span>` : "";
|
|
1624
|
+
/**
|
|
1625
|
+
* Cascata defensiva pra NUNCA renderizar uma row sem texto:
|
|
1626
|
+
* 1) campo específico do tipo (name pra milestone, message pra error, …)
|
|
1627
|
+
* 2) payload.label
|
|
1628
|
+
* 3) payload.title
|
|
1629
|
+
* 4) humanizeTool(payload.tool)
|
|
1630
|
+
* 5) o tipo humanizado em si (último recurso — "Marco", "Erro", "Iniciado")
|
|
1631
|
+
*/
|
|
1632
|
+
const fallback = (...candidates) => {
|
|
1633
|
+
for (const c of candidates) {
|
|
1634
|
+
const s = safeStr(typeof c === "string" ? c : (c == null ? "" : String(c)));
|
|
1635
|
+
if (s) return s;
|
|
1636
|
+
}
|
|
1637
|
+
return badge.toLowerCase();
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1300
1640
|
let msg = "";
|
|
1301
1641
|
switch (evt.type) {
|
|
1302
1642
|
case "run.start": {
|
|
1303
|
-
|
|
1643
|
+
const tool = safeStr(evt.payload?.tool || "");
|
|
1644
|
+
const title = humanizeTool(tool) || fallback(evt.payload?.title, evt.payload?.label, evt.payload?.name);
|
|
1645
|
+
const ident = tool ? ` <span class="ident">${escapeHtml(tool)}</span>` : "";
|
|
1646
|
+
const target = evt.payload?.target ? ` <span class="arrow">→</span> <span class="ident">${escapeHtml(safeStr(evt.payload.target))}</span>` : "";
|
|
1647
|
+
msg = `<strong>${escapeHtml(title)}</strong>${ident}${target}`;
|
|
1304
1648
|
break;
|
|
1305
1649
|
}
|
|
1306
1650
|
case "run.end": {
|
|
1307
1651
|
const dur = evt.payload?.duration_ms;
|
|
1308
|
-
|
|
1652
|
+
const tool = safeStr(evt.payload?.tool || "");
|
|
1653
|
+
const title = humanizeTool(tool) || fallback(evt.payload?.title, evt.payload?.label, evt.payload?.name);
|
|
1654
|
+
msg = `<strong>${escapeHtml(title)}</strong> ${ok ? "concluído" : "falhou"}${dur ? ` <span class="ident">${(dur/1000).toFixed(2)}s</span>` : ""}`;
|
|
1309
1655
|
break;
|
|
1310
1656
|
}
|
|
1311
1657
|
case "progress": {
|
|
1312
1658
|
const pct = typeof evt.payload?.percent === "number" ? `${Math.round(evt.payload.percent)}%` : "";
|
|
1313
|
-
const lbl = evt.payload?.label
|
|
1659
|
+
const lbl = fallback(evt.payload?.label, evt.payload?.title, evt.payload?.name, "em andamento");
|
|
1314
1660
|
const ct = evt.payload?.current && evt.payload?.total ? ` <span class="ident">${evt.payload.current}/${evt.payload.total}</span>` : "";
|
|
1315
1661
|
msg = `${pct ? `<strong>${pct}</strong> ` : ""}<span class="path">${escapeHtml(humanizePath(lbl))}</span>${ct}`;
|
|
1316
1662
|
break;
|
|
1317
1663
|
}
|
|
1318
1664
|
case "milestone": {
|
|
1319
|
-
|
|
1665
|
+
const lbl = fallback(evt.payload?.name, evt.payload?.title, evt.payload?.label, "marco");
|
|
1666
|
+
msg = `<strong>${escapeHtml(lbl)}</strong>`;
|
|
1320
1667
|
break;
|
|
1321
1668
|
}
|
|
1322
1669
|
case "error": {
|
|
1323
|
-
|
|
1670
|
+
const lbl = fallback(evt.payload?.message, evt.payload?.title, evt.payload?.label, evt.payload?.name, "erro");
|
|
1671
|
+
const code = evt.payload?.code ? ` <span class="ident">${escapeHtml(safeStr(evt.payload.code))}</span>` : "";
|
|
1672
|
+
msg = `<strong>${escapeHtml(lbl)}</strong>${code}`;
|
|
1324
1673
|
break;
|
|
1325
1674
|
}
|
|
1326
1675
|
case "tool_invocation": {
|
|
1327
|
-
|
|
1676
|
+
const tool = safeStr(evt.payload?.tool || "");
|
|
1677
|
+
const title = humanizeTool(tool) || fallback(evt.payload?.title, evt.payload?.label, "ferramenta invocada");
|
|
1678
|
+
msg = `<strong>${escapeHtml(title)}</strong>${tool ? ` <span class="ident">${escapeHtml(tool)}</span>` : ""}`;
|
|
1679
|
+
break;
|
|
1680
|
+
}
|
|
1681
|
+
case "shutdown": {
|
|
1682
|
+
msg = `<strong>${escapeHtml(fallback(evt.payload?.message, "sidecar encerrado"))}</strong>`;
|
|
1328
1683
|
break;
|
|
1329
1684
|
}
|
|
1330
1685
|
default:
|
|
1331
|
-
msg = ""
|
|
1686
|
+
msg = `<span class="ident">${escapeHtml(badge.toLowerCase())}</span>`;
|
|
1332
1687
|
}
|
|
1333
1688
|
return `
|
|
1334
1689
|
<div class="tl-row" data-type="${evt.type}" data-ok="${ok}" data-grouped="${grouped}">
|
|
@@ -1337,6 +1692,7 @@ function rowHtml(evt, idx, prev) {
|
|
|
1337
1692
|
<div class="tl-content">
|
|
1338
1693
|
<span class="tl-badge">${badge}</span>
|
|
1339
1694
|
<span class="tl-msg">${msg}</span>
|
|
1695
|
+
${tokenChip}
|
|
1340
1696
|
${evt.runId ? `<span class="tl-runid">${evt.runId.slice(0,6)}</span>` : ""}
|
|
1341
1697
|
</div>
|
|
1342
1698
|
</div>
|
|
@@ -1365,104 +1721,14 @@ setInterval(() => {
|
|
|
1365
1721
|
});
|
|
1366
1722
|
}, 1000);
|
|
1367
1723
|
|
|
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
|
-
|
|
1724
|
+
/* ───────────────────── view actions ───────────────────── */
|
|
1725
|
+
/* "limpar tela" zera o que está VISÍVEL (eventos correntes + cards ativos),
|
|
1726
|
+
mas NUNCA toca o histórico de runs concluídas — esse é o ponto do drawer. */
|
|
1461
1727
|
function clearAll() {
|
|
1462
|
-
clearMock();
|
|
1463
1728
|
state.events = [];
|
|
1464
1729
|
state.runs.clear();
|
|
1465
1730
|
state._seenKeys = new Set();
|
|
1731
|
+
state.totalTokens = 0;
|
|
1466
1732
|
els.active.innerHTML = "";
|
|
1467
1733
|
render();
|
|
1468
1734
|
els.lastSeen.textContent = "aguardando…";
|
|
@@ -1498,14 +1764,30 @@ function wireSeg(rootSel, attr, fn) {
|
|
|
1498
1764
|
}
|
|
1499
1765
|
wireSeg("#tw-density", "v", (v) => document.documentElement.dataset.density = v);
|
|
1500
1766
|
wireSeg("#tw-motion", "v", (v) => document.documentElement.dataset.motion = v);
|
|
1501
|
-
wireSeg("#tw-scenario","v", (v) => { clearAll(); runScenario(v); });
|
|
1502
1767
|
|
|
1503
1768
|
document.documentElement.dataset.density = "normal";
|
|
1504
1769
|
document.documentElement.dataset.motion = "medium";
|
|
1505
1770
|
|
|
1506
|
-
document.getElementById("tw-replay").addEventListener("click", () => { clearAll(); runScenario(currentScenario); });
|
|
1507
1771
|
document.getElementById("tw-clear").addEventListener("click", clearAll);
|
|
1508
1772
|
|
|
1773
|
+
/* ───────────────────── history wiring ───────────────────── */
|
|
1774
|
+
els.historyBtn.addEventListener("click", () => {
|
|
1775
|
+
const open = els.history.classList.toggle("open");
|
|
1776
|
+
els.historyBtn.setAttribute("aria-pressed", open);
|
|
1777
|
+
if (open) renderHistory();
|
|
1778
|
+
});
|
|
1779
|
+
els.historyClose.addEventListener("click", () => {
|
|
1780
|
+
els.history.classList.remove("open");
|
|
1781
|
+
els.historyBtn.setAttribute("aria-pressed", "false");
|
|
1782
|
+
});
|
|
1783
|
+
// Esc fecha o drawer se ele estiver aberto e nada mais focado
|
|
1784
|
+
document.addEventListener("keydown", (e) => {
|
|
1785
|
+
if (e.key === "Escape" && els.history.classList.contains("open") && document.activeElement !== els.q) {
|
|
1786
|
+
els.history.classList.remove("open");
|
|
1787
|
+
els.historyBtn.setAttribute("aria-pressed", "false");
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1509
1791
|
/* ───────────────────── toolbar wiring ───────────────────── */
|
|
1510
1792
|
els.q.addEventListener("input", (e) => { state.filterText = e.target.value.trim(); render(); });
|
|
1511
1793
|
document.addEventListener("keydown", (e) => {
|
|
@@ -1671,22 +1953,14 @@ function showShutdownBanner() {
|
|
|
1671
1953
|
}
|
|
1672
1954
|
|
|
1673
1955
|
/* ───────────────────── boot ───────────────────── */
|
|
1674
|
-
/*
|
|
1675
|
-
source of truth. The mock scenarios are still wired and accessible through
|
|
1676
|
-
the tweaks panel for demo/dev. */
|
|
1956
|
+
/* `/events` SSE é a única fonte de verdade. Nenhum mock no boot. */
|
|
1677
1957
|
|
|
1958
|
+
loadHistory(); // restaura o histórico desta sessão (sessionStorage)
|
|
1678
1959
|
(async () => {
|
|
1679
1960
|
await hydrateFromState();
|
|
1680
1961
|
connectRealSource();
|
|
1962
|
+
render(); // pinta footer + drawer mesmo antes do primeiro evento real
|
|
1681
1963
|
})();
|
|
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
1964
|
</script>
|
|
1691
1965
|
</body>
|
|
1692
1966
|
</html>
|