@luanpdd/kit-mcp 1.2.3 → 1.4.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.
@@ -1,1022 +1,1692 @@
1
1
  <!doctype html>
2
- <html lang="en">
2
+ <html lang="pt-BR">
3
3
  <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>kit-mcp sidecar</title>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>kit-mcp · sidecar</title>
7
7
  <style>
8
- /* Design tokens */
8
+ /* ───────────────────────── tokens ───────────────────────── */
9
+ :root {
10
+ /* surfaces */
11
+ --bg: #000000;
12
+ --surface-1: #0b0d10;
13
+ --surface-2: #11141a;
14
+ --surface-3: #171b22;
15
+ --hover: #1c2129;
16
+
17
+ /* lines + text */
18
+ --line: rgba(255, 255, 255, 0.06);
19
+ --line-strong: rgba(255, 255, 255, 0.12);
20
+ --text: #e6e8eb;
21
+ --text-2: #a8adb6;
22
+ --text-3: #6c7280;
23
+ --text-4: #4a4f57;
24
+
25
+ /* states */
26
+ --accent-h: 130;
27
+ --accent: oklch(82% 0.18 var(--accent-h));
28
+ --accent-soft: oklch(82% 0.18 var(--accent-h) / 0.16);
29
+ --accent-glow: oklch(82% 0.18 var(--accent-h) / 0.42);
30
+ --ok: #d4d8df;
31
+ --err: oklch(70% 0.18 25);
32
+ --err-soft: oklch(70% 0.18 25 / 0.14);
33
+ --warn: oklch(82% 0.14 75);
34
+ --info: oklch(78% 0.10 250);
35
+
36
+ /* type */
37
+ --sans: -apple-system, "Segoe UI", system-ui, "Helvetica Neue", Arial, sans-serif;
38
+ --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
39
+
40
+ /* shape */
41
+ --radius: 10px;
42
+ --radius-sm: 6px;
43
+
44
+ /* motion */
45
+ --ease: cubic-bezier(.2,.7,.2,1);
46
+
47
+ /* layout */
48
+ --pad: 16px;
49
+ --pad-tight: 10px;
50
+ }
51
+
52
+ @media (prefers-color-scheme: light) {
9
53
  :root {
10
- color-scheme: light dark;
11
- --bg: #f8fafc;
12
- --bg-elev: #ffffff;
13
- --bg-row: #ffffff;
14
- --bg-row-hover: #f1f5f9;
15
- --fg: #0f172a;
16
- --fg-muted: #475569;
17
- --fg-subtle: #64748b;
18
- --border: #e2e8f0;
19
- --accent: #3b82f6;
20
- --ok: #10b981;
21
- --warn: #f59e0b;
22
- --err: #ef4444;
23
- --info: #6366f1;
24
- --shadow: 0 1px 2px rgba(0,0,0,.04), 0 4px 10px rgba(0,0,0,.04);
25
- --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
26
- --sans: system-ui, -apple-system, "Segoe UI", Roboto, Inter, "Helvetica Neue", sans-serif;
27
- }
28
- @media (prefers-color-scheme: dark) {
29
- :root {
30
- --bg: #0b1120;
31
- --bg-elev: #111827;
32
- --bg-row: #0f172a;
33
- --bg-row-hover: #1e293b;
34
- --fg: #e2e8f0;
35
- --fg-muted: #94a3b8;
36
- --fg-subtle: #64748b;
37
- --border: #1f2937;
38
- --accent: #60a5fa;
39
- --ok: #34d399;
40
- --warn: #fbbf24;
41
- --err: #f87171;
42
- --info: #818cf8;
43
- --shadow: 0 1px 2px rgba(0,0,0,.3), 0 4px 10px rgba(0,0,0,.3);
44
- }
45
- }
46
-
47
- * { box-sizing: border-box; }
48
- html, body { margin: 0; padding: 0; height: 100%; }
49
- body {
50
- font-family: var(--sans);
51
- font-size: 14px;
52
- line-height: 1.5;
53
- color: var(--fg);
54
- background: var(--bg);
55
- display: flex;
56
- flex-direction: column;
57
- }
58
-
59
- header {
60
- display: flex;
61
- align-items: center;
62
- gap: 12px;
63
- padding: 10px 16px;
64
- border-bottom: 1px solid var(--border);
65
- background: var(--bg-elev);
66
- box-shadow: var(--shadow);
67
- position: sticky;
68
- top: 0;
69
- z-index: 10;
70
- }
71
- header h1 {
72
- margin: 0;
73
- font-size: 14px;
74
- font-weight: 600;
75
- letter-spacing: .2px;
76
- }
77
- header h1::before {
78
- content: "◆";
79
- color: var(--accent);
80
- margin-right: 6px;
81
- }
82
- header .meta {
83
- color: var(--fg-subtle);
84
- font-family: var(--mono);
85
- font-size: 12px;
86
- }
87
- header .grow { flex: 1; }
88
-
89
- .conn-status {
90
- display: inline-flex;
91
- align-items: center;
92
- gap: 6px;
93
- padding: 3px 10px;
94
- border-radius: 999px;
95
- font-family: var(--mono);
96
- font-size: 11px;
97
- font-weight: 600;
98
- border: 1px solid var(--border);
99
- background: var(--bg-row);
100
- }
101
- .conn-status .dot {
102
- display: inline-block;
103
- width: 8px;
104
- height: 8px;
105
- border-radius: 50%;
106
- background: var(--fg-subtle);
107
- }
108
- .conn-status[data-state="CONNECTING"] .dot { background: var(--warn); animation: pulse 1.4s infinite; }
109
- .conn-status[data-state="OPEN"] .dot { background: var(--ok); }
110
- .conn-status[data-state="CLOSED"] .dot { background: var(--err); animation: pulse 1s infinite; }
111
- @keyframes pulse { 50% { opacity: .35; } }
112
-
113
- .toolbar {
114
- display: flex;
115
- align-items: center;
116
- gap: 10px;
117
- padding: 10px 16px;
118
- border-bottom: 1px solid var(--border);
119
- background: var(--bg-elev);
120
- flex-wrap: wrap;
121
- }
122
- .toolbar input[type="search"] {
123
- flex: 1 1 240px;
124
- min-width: 180px;
125
- padding: 6px 10px;
126
- border: 1px solid var(--border);
127
- border-radius: 6px;
128
- background: var(--bg-row);
129
- color: var(--fg);
130
- font-family: var(--sans);
131
- font-size: 13px;
132
- }
133
- .toolbar input[type="search"]:focus {
134
- outline: 2px solid var(--accent);
135
- outline-offset: -1px;
136
- }
137
- .filters {
138
- display: flex;
139
- flex-wrap: wrap;
140
- gap: 4px;
141
- }
142
- .filters label {
143
- display: inline-flex;
144
- align-items: center;
145
- gap: 4px;
146
- padding: 3px 8px;
147
- border-radius: 999px;
148
- border: 1px solid var(--border);
149
- background: var(--bg-row);
150
- cursor: pointer;
151
- user-select: none;
152
- font-size: 11px;
153
- font-family: var(--mono);
154
- }
155
- .filters input { display: none; }
156
- .filters label:has(input:checked) {
157
- background: var(--accent);
158
- color: white;
159
- border-color: var(--accent);
160
- }
161
-
162
- button {
163
- padding: 5px 12px;
164
- border: 1px solid var(--border);
165
- border-radius: 6px;
166
- background: var(--bg-row);
167
- color: var(--fg);
168
- font-family: var(--sans);
169
- font-size: 12px;
170
- cursor: pointer;
171
- }
172
- button:hover { background: var(--bg-row-hover); }
173
- button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
174
- button[aria-pressed="true"] { background: var(--accent); color: white; border-color: var(--accent); }
175
-
176
- main {
177
- flex: 1;
178
- overflow: auto;
179
- padding: 0 16px 24px;
180
- }
181
-
182
- .empty {
183
- text-align: center;
184
- color: var(--fg-subtle);
185
- padding: 64px 16px;
186
- font-size: 14px;
187
- }
188
- .empty strong { display: block; color: var(--fg-muted); margin-bottom: 6px; }
189
- .empty code {
190
- font-family: var(--mono);
191
- background: var(--bg-row);
192
- padding: 1px 6px;
193
- border-radius: 4px;
194
- font-size: 12px;
195
- border: 1px solid var(--border);
196
- }
197
-
198
- .ev-list {
199
- list-style: none;
200
- margin: 12px 0 0;
201
- padding: 0;
202
- border: 1px solid var(--border);
203
- border-radius: 8px;
204
- overflow: hidden;
205
- background: var(--bg-elev);
206
- }
207
- .ev-row {
208
- display: grid;
209
- grid-template-columns: 92px 110px 1fr;
210
- gap: 12px;
211
- padding: 8px 12px;
212
- border-bottom: 1px solid var(--border);
213
- font-size: 13px;
214
- align-items: start;
215
- }
216
- .ev-row:last-child { border-bottom: 0; }
217
- .ev-row:hover { background: var(--bg-row-hover); }
218
-
219
- .ev-time {
220
- font-family: var(--mono);
221
- font-size: 11px;
222
- color: var(--fg-subtle);
223
- padding-top: 1px;
224
- }
225
-
226
- .ev-badge {
227
- display: inline-block;
228
- padding: 2px 8px;
229
- border-radius: 999px;
230
- background: var(--bg-row);
231
- color: var(--fg-muted);
232
- font-family: var(--mono);
233
- font-size: 10px;
234
- font-weight: 700;
235
- text-transform: uppercase;
236
- letter-spacing: .5px;
237
- border: 1px solid var(--border);
238
- white-space: nowrap;
239
- text-align: center;
240
- }
241
- .ev-badge[data-type="run.start"] { color: var(--info); border-color: var(--info); }
242
- .ev-badge[data-type="run.end"] { color: var(--info); border-color: var(--info); }
243
- .ev-badge[data-type="tool_invocation"]{ color: var(--accent); border-color: var(--accent); }
244
- .ev-badge[data-type="progress"] { color: var(--ok); border-color: var(--ok); }
245
- .ev-badge[data-type="milestone"] { color: var(--warn); border-color: var(--warn); }
246
- .ev-badge[data-type="error"] { color: var(--err); border-color: var(--err); background: color-mix(in srgb, var(--err) 8%, transparent); }
247
- .ev-badge[data-type="shutdown"] { color: var(--err); border-color: var(--err); }
248
-
249
- .ev-body { min-width: 0; word-wrap: break-word; }
250
- .ev-body summary {
251
- list-style: none;
252
- cursor: pointer;
253
- }
254
- .ev-body summary::-webkit-details-marker { display: none; }
255
- .ev-body summary .label {
256
- color: var(--fg);
257
- font-family: var(--sans);
258
- }
259
- .ev-body summary .meta {
260
- color: var(--fg-subtle);
261
- font-family: var(--mono);
262
- font-size: 11px;
263
- margin-left: 6px;
264
- }
265
- .ev-body pre {
266
- margin: 6px 0 0;
267
- padding: 8px 10px;
268
- background: var(--bg-row);
269
- border: 1px solid var(--border);
270
- border-radius: 6px;
271
- font-family: var(--mono);
272
- font-size: 11px;
273
- color: var(--fg-muted);
274
- overflow-x: auto;
275
- max-height: 280px;
276
- }
277
-
278
- /* Active runs panel — current executions with live progress bars */
279
- .runs {
280
- margin: 12px 0 16px;
281
- display: grid;
282
- gap: 10px;
283
- }
284
- .runs[hidden] { display: none; }
285
- .runs-header {
286
- display: flex;
287
- align-items: center;
288
- gap: 8px;
289
- color: var(--fg-subtle);
290
- font-family: var(--mono);
291
- font-size: 11px;
292
- text-transform: uppercase;
293
- letter-spacing: .8px;
294
- }
295
- .runs-header .count {
296
- background: var(--accent);
297
- color: white;
298
- padding: 1px 8px;
299
- border-radius: 999px;
300
- font-weight: 700;
301
- }
302
-
303
- .run-card {
304
- background: var(--bg-elev);
305
- border: 1px solid var(--border);
306
- border-radius: 10px;
307
- padding: 14px 16px;
308
- box-shadow: var(--shadow);
309
- display: grid;
310
- grid-template-columns: minmax(0, 1fr) auto;
311
- gap: 10px 16px;
312
- align-items: baseline;
313
- position: relative;
314
- overflow: hidden;
315
- transition: opacity .25s ease;
316
- min-width: 0;
317
- }
318
- .run-card > * { min-width: 0; }
319
- .run-card[data-status="running"] {
320
- border-color: var(--accent);
321
- border-left-width: 4px;
322
- }
323
- .run-card[data-status="done"] {
324
- border-color: var(--ok);
325
- border-left-width: 4px;
326
- }
327
- .run-card[data-status="error"] {
328
- border-color: var(--err);
329
- border-left-width: 4px;
330
- background: color-mix(in srgb, var(--err) 5%, var(--bg-elev));
331
- }
332
- .run-card.fading { opacity: .4; }
333
-
334
- .run-card .run-title {
335
- font-family: var(--sans);
336
- font-weight: 600;
337
- font-size: 14px;
338
- color: var(--fg);
339
- display: flex;
340
- align-items: center;
341
- gap: 8px;
342
- }
343
- .run-card .run-kind {
344
- font-size: 11px;
345
- font-family: var(--mono);
346
- color: var(--fg-subtle);
347
- text-transform: uppercase;
348
- letter-spacing: .5px;
349
- background: var(--bg-row);
350
- padding: 1px 8px;
351
- border-radius: 999px;
352
- border: 1px solid var(--border);
353
- }
354
- .run-card .run-percent {
355
- font-family: var(--mono);
356
- font-size: 22px;
357
- font-weight: 700;
358
- font-variant-numeric: tabular-nums;
359
- color: var(--accent);
360
- line-height: 1;
361
- }
362
- .run-card[data-status="done"] .run-percent { color: var(--ok); }
363
- .run-card[data-status="error"] .run-percent { color: var(--err); font-size: 13px; }
364
-
365
- .run-card .run-bar-wrap {
366
- grid-column: 1 / -1;
367
- height: 8px;
368
- background: var(--bg-row);
369
- border-radius: 4px;
370
- overflow: hidden;
371
- border: 1px solid var(--border);
372
- position: relative;
373
- }
374
- .run-card .run-bar {
375
- height: 100%;
376
- background: var(--accent);
377
- border-radius: 3px;
378
- transition: width .25s ease;
379
- position: relative;
380
- }
381
- .run-card[data-status="done"] .run-bar { background: var(--ok); }
382
- .run-card[data-status="error"] .run-bar { background: var(--err); }
383
- .run-card[data-status="running"] .run-bar::after {
384
- /* Subtle moving stripe so the bar feels alive even when % stays put briefly */
385
- content: '';
386
- position: absolute; inset: 0;
387
- background-image: linear-gradient(
388
- 90deg,
389
- transparent 0%, transparent 40%,
390
- color-mix(in srgb, white 25%, transparent) 50%,
391
- transparent 60%, transparent 100%
392
- );
393
- background-size: 200% 100%;
394
- animation: shimmer 1.6s linear infinite;
395
- }
396
- @keyframes shimmer {
397
- 0% { background-position: 200% 0; }
398
- 100% { background-position: -200% 0; }
399
- }
400
-
401
- .run-card .run-step {
402
- grid-column: 1 / -1;
403
- color: var(--fg-muted);
404
- font-family: var(--mono);
405
- font-size: 12px;
406
- word-break: break-word;
407
- }
408
- .run-card .run-meta {
409
- grid-column: 1 / -1;
410
- display: flex;
411
- gap: 12px;
412
- color: var(--fg-subtle);
413
- font-family: var(--mono);
414
- font-size: 10px;
415
- }
416
- .run-card .run-meta .runid { letter-spacing: .5px; }
417
- .run-card[data-status="error"] .run-step { color: var(--err); }
418
-
419
- .banner {
420
- margin: 12px 0 0;
421
- padding: 12px 16px;
422
- border-radius: 8px;
423
- background: color-mix(in srgb, var(--err) 8%, var(--bg-elev));
424
- border: 1px solid var(--err);
425
- color: var(--err);
426
- font-size: 13px;
427
- }
428
- .banner[hidden] { display: none; }
429
- .banner code {
430
- font-family: var(--mono);
431
- background: var(--bg-row);
432
- padding: 1px 5px;
433
- border-radius: 4px;
434
- color: var(--fg-muted);
435
- }
436
-
437
- .footer {
438
- padding: 8px 16px;
439
- border-top: 1px solid var(--border);
440
- background: var(--bg-elev);
441
- color: var(--fg-subtle);
442
- font-family: var(--mono);
443
- font-size: 11px;
444
- display: flex;
445
- gap: 16px;
446
- flex-wrap: wrap;
447
- }
54
+ --bg: #fbfbfa;
55
+ --surface-1: #ffffff;
56
+ --surface-2: #f7f7f6;
57
+ --surface-3: #f1f1f0;
58
+ --hover: #ebebea;
59
+ --line: rgba(0,0,0,.07);
60
+ --line-strong: rgba(0,0,0,.14);
61
+ --text: #15171a;
62
+ --text-2: #54585f;
63
+ --text-3: #80858d;
64
+ --text-4: #a8adb4;
65
+ --accent: oklch(58% 0.18 var(--accent-h));
66
+ --accent-soft: oklch(58% 0.18 var(--accent-h) / 0.10);
67
+ --accent-glow: oklch(58% 0.18 var(--accent-h) / 0.30);
68
+ }
69
+ }
70
+
71
+ /* density tweak */
72
+ :root[data-density="compact"] { --pad: 10px; --pad-tight: 6px; }
73
+ :root[data-density="comfy"] { --pad: 20px; --pad-tight: 14px; }
74
+
75
+ /* ───────────────────────── base ───────────────────────── */
76
+ * { box-sizing: border-box; }
77
+ html, body {
78
+ margin: 0;
79
+ background: var(--bg);
80
+ color: var(--text);
81
+ font-family: var(--sans);
82
+ font-size: 13px;
83
+ line-height: 1.4;
84
+ -webkit-font-smoothing: antialiased;
85
+ text-rendering: optimizeLegibility;
86
+ }
87
+ body {
88
+ min-height: 100vh;
89
+ font-feature-settings: "ss01", "cv11";
90
+ }
91
+ button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 0; }
92
+ ::selection { background: var(--accent-soft); color: var(--text); }
93
+
94
+ /* scrollbar */
95
+ ::-webkit-scrollbar { width: 10px; height: 10px; }
96
+ ::-webkit-scrollbar-track { background: transparent; }
97
+ ::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 10px; border: 2px solid var(--bg); }
98
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-4); }
99
+
100
+ /* ───────────────────────── shell ───────────────────────── */
101
+ .app {
102
+ display: grid;
103
+ grid-template-rows: auto auto 1fr auto;
104
+ min-height: 100vh;
105
+ max-width: 980px;
106
+ margin: 0 auto;
107
+ padding: 18px 22px 0;
108
+ }
109
+
110
+ @media (max-width: 520px) {
111
+ .app { padding: 12px 14px 0; }
112
+ }
113
+
114
+ /* ───────────────────────── header ───────────────────────── */
115
+ .header {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 12px;
119
+ padding: 6px 0 16px;
120
+ border-bottom: 1px solid var(--line);
121
+ margin-bottom: 16px;
122
+ }
123
+ .logo {
124
+ width: 22px; height: 22px;
125
+ display: grid; place-items: center;
126
+ border-radius: 6px;
127
+ background: var(--accent-soft);
128
+ color: var(--accent);
129
+ flex-shrink: 0;
130
+ }
131
+ .logo svg { width: 14px; height: 14px; }
132
+ .brand {
133
+ display: flex; flex-direction: column; gap: 1px;
134
+ min-width: 0;
135
+ }
136
+ .brand-row {
137
+ display: flex; align-items: baseline; gap: 8px;
138
+ font-family: var(--mono);
139
+ font-size: 12px;
140
+ letter-spacing: -0.01em;
141
+ }
142
+ .brand-name { color: var(--text); font-weight: 500; }
143
+ .brand-meta { color: var(--text-3); font-size: 11px; }
144
+ .brand-sub {
145
+ font-size: 11px;
146
+ color: var(--text-3);
147
+ }
148
+ .spacer { flex: 1; }
149
+ .conn {
150
+ display: inline-flex; align-items: center; gap: 6px;
151
+ font-family: var(--mono);
152
+ font-size: 11px;
153
+ color: var(--text-2);
154
+ padding: 4px 8px;
155
+ border: 1px solid var(--line);
156
+ border-radius: 999px;
157
+ background: var(--surface-1);
158
+ }
159
+ .conn .dot {
160
+ width: 6px; height: 6px; border-radius: 999px;
161
+ background: var(--accent);
162
+ box-shadow: 0 0 0 0 var(--accent-glow);
163
+ animation: connpulse 2.4s ease-out infinite;
164
+ }
165
+ @keyframes connpulse {
166
+ 0% { box-shadow: 0 0 0 0 var(--accent-glow); }
167
+ 70% { box-shadow: 0 0 0 6px transparent; }
168
+ 100% { box-shadow: 0 0 0 0 transparent; }
169
+ }
170
+ .conn[data-state="off"] .dot { background: var(--text-4); animation: none; }
171
+ .conn[data-state="off"] { color: var(--text-3); }
172
+
173
+ /* ───────────────────────── toolbar ───────────────────────── */
174
+ .toolbar {
175
+ display: flex; align-items: center; gap: 8px;
176
+ margin-bottom: 16px;
177
+ }
178
+ .search {
179
+ flex: 1;
180
+ display: flex; align-items: center; gap: 8px;
181
+ padding: 7px 10px;
182
+ border: 1px solid var(--line);
183
+ border-radius: var(--radius-sm);
184
+ background: var(--surface-1);
185
+ transition: border-color .15s var(--ease), background .15s var(--ease);
186
+ }
187
+ .search:focus-within { border-color: var(--line-strong); background: var(--surface-2); }
188
+ .search svg { width: 13px; height: 13px; color: var(--text-3); flex-shrink: 0; }
189
+ .search input {
190
+ flex: 1;
191
+ background: transparent; border: 0; outline: 0; color: var(--text);
192
+ font: inherit;
193
+ font-size: 12px;
194
+ }
195
+ .search input::placeholder { color: var(--text-3); }
196
+ .search kbd {
197
+ font-family: var(--mono);
198
+ font-size: 10px;
199
+ padding: 2px 5px;
200
+ border-radius: 3px;
201
+ background: var(--surface-3);
202
+ border: 1px solid var(--line);
203
+ color: var(--text-3);
204
+ }
205
+
206
+ .iconbtn {
207
+ display: grid; place-items: center;
208
+ width: 30px; height: 30px;
209
+ border-radius: var(--radius-sm);
210
+ border: 1px solid var(--line);
211
+ background: var(--surface-1);
212
+ color: var(--text-2);
213
+ transition: all .15s var(--ease);
214
+ }
215
+ .iconbtn:hover { color: var(--text); background: var(--surface-2); border-color: var(--line-strong); }
216
+ .iconbtn[aria-pressed="true"] { color: var(--accent); border-color: var(--accent-soft); background: var(--accent-soft); }
217
+ .iconbtn svg { width: 13px; height: 13px; }
218
+
219
+ /* filter popover */
220
+ .filter-wrap { position: relative; }
221
+ .filter-pop {
222
+ position: absolute;
223
+ top: calc(100% + 6px); right: 0;
224
+ min-width: 200px;
225
+ padding: 6px;
226
+ background: var(--surface-2);
227
+ border: 1px solid var(--line-strong);
228
+ border-radius: var(--radius);
229
+ box-shadow: 0 8px 28px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.02);
230
+ z-index: 30;
231
+ display: none;
232
+ }
233
+ .filter-pop.open { display: block; animation: pop-in .14s var(--ease); }
234
+ @keyframes pop-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
235
+ .fp-title { font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--text-3); padding: 6px 8px 4px; }
236
+ .fp-row {
237
+ display: flex; align-items: center; gap: 8px;
238
+ padding: 6px 8px;
239
+ border-radius: 6px;
240
+ font-size: 12px;
241
+ color: var(--text-2);
242
+ cursor: pointer;
243
+ user-select: none;
244
+ }
245
+ .fp-row:hover { background: var(--surface-3); color: var(--text); }
246
+ .fp-row[data-on="true"] { color: var(--text); }
247
+ .fp-row[data-on="true"] .fp-check { color: var(--accent); }
248
+ .fp-check { width: 12px; opacity: 0; }
249
+ .fp-row[data-on="true"] .fp-check { opacity: 1; }
250
+ .fp-row .fp-glyph { font-family: var(--mono); width: 12px; text-align: center; color: var(--text-3); }
251
+
252
+ /* ───────────────────────── active runs hero ───────────────────────── */
253
+ .active-region {
254
+ margin-bottom: 18px;
255
+ display: flex; flex-direction: column; gap: 10px;
256
+ }
257
+
258
+ .run-card {
259
+ position: relative;
260
+ padding: var(--pad);
261
+ background:
262
+ radial-gradient(120% 80% at 0% 0%, var(--accent-soft) 0%, transparent 45%),
263
+ var(--surface-1);
264
+ border: 1px solid var(--line);
265
+ border-radius: var(--radius);
266
+ overflow: hidden;
267
+ isolation: isolate;
268
+ }
269
+ .run-card.enter { animation: run-in .35s var(--ease); }
270
+ @keyframes run-in {
271
+ from { opacity: 0; transform: translateY(6px) scale(.995); }
272
+ to { opacity: 1; transform: none; }
273
+ }
274
+
275
+ /* ambient running glow on the card edge */
276
+ .run-card::before {
277
+ content: "";
278
+ position: absolute; inset: -1px;
279
+ border-radius: inherit;
280
+ padding: 1px;
281
+ background: conic-gradient(from var(--ang, 0deg),
282
+ transparent 0deg, transparent 280deg,
283
+ var(--accent) 320deg, var(--accent) 340deg, transparent 360deg);
284
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
285
+ -webkit-mask-composite: xor;
286
+ mask-composite: exclude;
287
+ opacity: .8;
288
+ pointer-events: none;
289
+ animation: spin 4s linear infinite;
290
+ }
291
+ @property --ang { syntax: "<angle>"; initial-value: 0deg; inherits: false; }
292
+ @keyframes spin { to { --ang: 360deg; } }
293
+
294
+ :root[data-motion="subtle"] .run-card::before { animation: none; opacity: .4; }
295
+
296
+ .rc-head {
297
+ display: flex; align-items: center; gap: 10px;
298
+ margin-bottom: 12px;
299
+ }
300
+ .rc-icon {
301
+ width: 32px; height: 32px;
302
+ border-radius: 8px;
303
+ display: grid; place-items: center;
304
+ background: var(--surface-3);
305
+ color: var(--text);
306
+ font-family: var(--mono);
307
+ font-size: 12px;
308
+ font-weight: 600;
309
+ border: 1px solid var(--line-strong);
310
+ position: relative;
311
+ flex-shrink: 0;
312
+ }
313
+ .rc-icon svg { width: 15px; height: 15px; }
314
+ .rc-icon[data-tool="sync"] { color: var(--accent); }
315
+ .rc-icon[data-tool="reverse"] { color: oklch(78% 0.14 280); }
316
+ .rc-icon[data-tool="gates"] { color: oklch(82% 0.14 75); }
317
+
318
+ .rc-title-block { min-width: 0; flex: 1; }
319
+ .rc-tool {
320
+ font-family: var(--mono);
321
+ font-size: 11px;
322
+ color: var(--text-3);
323
+ letter-spacing: -0.01em;
324
+ }
325
+ .rc-title {
326
+ font-size: 16px;
327
+ font-weight: 500;
328
+ color: var(--text);
329
+ letter-spacing: -0.01em;
330
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
331
+ }
332
+ .rc-elapsed {
333
+ font-family: var(--mono);
334
+ font-size: 12px;
335
+ color: var(--text-2);
336
+ text-align: right;
337
+ font-variant-numeric: tabular-nums;
338
+ flex-shrink: 0;
339
+ display: flex; flex-direction: column; align-items: flex-end; gap: 2px;
340
+ }
341
+ .rc-elapsed .em { color: var(--accent); }
342
+ .rc-elapsed .em.warn { color: var(--warn); }
343
+ .rc-elapsed small { color: var(--text-3); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; }
344
+
345
+ /* progress */
346
+ .rc-progress {
347
+ display: flex; align-items: center; gap: 12px;
348
+ margin-bottom: 10px;
349
+ }
350
+ .rc-bar {
351
+ flex: 1;
352
+ height: 6px;
353
+ border-radius: 999px;
354
+ background: var(--surface-3);
355
+ overflow: hidden;
356
+ position: relative;
357
+ }
358
+ .rc-bar-fill {
359
+ height: 100%;
360
+ background: linear-gradient(90deg, var(--accent) 0%, oklch(88% 0.16 calc(var(--accent-h) + 30)) 100%);
361
+ border-radius: inherit;
362
+ transition: width .6s var(--ease);
363
+ position: relative;
364
+ }
365
+ .rc-bar-fill::after {
366
+ content: "";
367
+ position: absolute; inset: 0;
368
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.35) 50%, transparent 100%);
369
+ animation: shimmer 2s linear infinite;
370
+ mix-blend-mode: overlay;
371
+ }
372
+ @keyframes shimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
373
+ :root[data-motion="subtle"] .rc-bar-fill::after { display: none; }
374
+
375
+ .rc-pct {
376
+ font-family: var(--mono);
377
+ font-size: 13px;
378
+ color: var(--text);
379
+ font-variant-numeric: tabular-nums;
380
+ min-width: 38px;
381
+ text-align: right;
382
+ }
383
+
384
+ /* current step caption */
385
+ .rc-step {
386
+ display: flex; align-items: center; gap: 8px;
387
+ font-family: var(--mono);
388
+ font-size: 12px;
389
+ color: var(--text-2);
390
+ padding: 8px 10px;
391
+ background: var(--surface-2);
392
+ border: 1px solid var(--line);
393
+ border-radius: 6px;
394
+ white-space: nowrap; overflow: hidden;
395
+ }
396
+ .rc-step .glyph {
397
+ color: var(--accent);
398
+ display: inline-grid; place-items: center;
399
+ width: 12px; height: 12px;
400
+ flex-shrink: 0;
401
+ }
402
+ .rc-step .glyph svg { width: 12px; height: 12px; animation: spin-slow 1.4s linear infinite; }
403
+ @keyframes spin-slow { to { transform: rotate(360deg); } }
404
+
405
+ .rc-step-text {
406
+ flex: 1;
407
+ overflow: hidden; text-overflow: ellipsis;
408
+ transition: opacity .25s var(--ease);
409
+ }
410
+ .rc-step-text.fade { opacity: 0.3; }
411
+
412
+ .rc-step-count {
413
+ color: var(--text-3);
414
+ font-variant-numeric: tabular-nums;
415
+ font-size: 11px;
416
+ flex-shrink: 0;
417
+ }
418
+
419
+ /* run id chip */
420
+ .rc-foot {
421
+ display: flex; align-items: center; gap: 10px;
422
+ margin-top: 10px;
423
+ font-family: var(--mono);
424
+ font-size: 10px;
425
+ color: var(--text-3);
426
+ }
427
+ .rc-runid {
428
+ padding: 2px 6px;
429
+ border-radius: 4px;
430
+ background: var(--surface-3);
431
+ border: 1px solid var(--line);
432
+ color: var(--text-2);
433
+ }
434
+ .rc-foot .sep { color: var(--text-4); }
435
+
436
+ /* multiple active runs => stack 'em */
437
+ .active-region[data-count="2"] .run-card,
438
+ .active-region[data-count="3"] .run-card { padding: 12px; }
439
+ .active-region[data-count="2"] .run-card .rc-title,
440
+ .active-region[data-count="3"] .run-card .rc-title { font-size: 14px; }
441
+
442
+ /* ───────────────────────── log section ───────────────────────── */
443
+ .section-head {
444
+ display: flex; align-items: baseline; gap: 8px;
445
+ margin: 8px 0 10px;
446
+ padding: 0 2px;
447
+ }
448
+ .section-title {
449
+ font-family: var(--mono);
450
+ font-size: 10px;
451
+ text-transform: uppercase;
452
+ letter-spacing: .12em;
453
+ color: var(--text-3);
454
+ }
455
+ .section-count {
456
+ font-family: var(--mono);
457
+ font-size: 10px;
458
+ color: var(--text-4);
459
+ }
460
+
461
+ /* timeline */
462
+ .timeline {
463
+ position: relative;
464
+ padding: 0 0 32px 0;
465
+ }
466
+ .tl-day {
467
+ display: flex; align-items: center; gap: 8px;
468
+ font-family: var(--mono);
469
+ font-size: 10px;
470
+ color: var(--text-3);
471
+ text-transform: uppercase;
472
+ letter-spacing: .08em;
473
+ padding: 14px 0 10px;
474
+ position: sticky;
475
+ top: 0;
476
+ background: linear-gradient(var(--bg) 70%, transparent);
477
+ z-index: 1;
478
+ }
479
+ .tl-day::before, .tl-day::after {
480
+ content: ""; flex: 1;
481
+ border-top: 1px dashed var(--line);
482
+ }
483
+
484
+ .tl-row {
485
+ display: grid;
486
+ grid-template-columns: 64px 18px 1fr;
487
+ gap: 0;
488
+ padding: var(--pad-tight) 0;
489
+ position: relative;
490
+ border-radius: 4px;
491
+ transition: background .15s var(--ease);
492
+ }
493
+ .tl-row.enter { animation: row-in .35s var(--ease); }
494
+ .tl-row:hover { background: var(--surface-1); }
495
+ @keyframes row-in {
496
+ from { opacity: 0; transform: translateX(-4px); }
497
+ to { opacity: 1; transform: none; }
498
+ }
499
+ .tl-row.is-new { background: var(--accent-soft); }
500
+ .tl-row.is-new::after {
501
+ content: "novo";
502
+ position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
503
+ font-family: var(--mono);
504
+ font-size: 9px;
505
+ text-transform: uppercase;
506
+ letter-spacing: .1em;
507
+ color: var(--accent);
508
+ animation: fade-out 2.4s 1s forwards;
509
+ }
510
+ @keyframes fade-out { to { opacity: 0; } }
511
+
512
+ .tl-time {
513
+ font-family: var(--mono);
514
+ font-size: 11px;
515
+ color: var(--text-3);
516
+ font-variant-numeric: tabular-nums;
517
+ padding-left: 4px;
518
+ padding-top: 1px;
519
+ }
520
+
521
+ /* rail with node */
522
+ .tl-rail {
523
+ position: relative;
524
+ display: flex; justify-content: center;
525
+ }
526
+ .tl-rail::before {
527
+ content: "";
528
+ position: absolute;
529
+ top: -10px; bottom: -10px;
530
+ left: 50%; width: 1px;
531
+ background: var(--line);
532
+ transform: translateX(-.5px);
533
+ }
534
+ .tl-row:first-of-type .tl-rail::before { top: 8px; }
535
+ .tl-row:last-of-type .tl-rail::before { bottom: calc(100% - 8px); }
536
+
537
+ .tl-node {
538
+ position: relative;
539
+ z-index: 1;
540
+ width: 7px; height: 7px;
541
+ border-radius: 999px;
542
+ margin-top: 5px;
543
+ background: var(--text-4);
544
+ border: 2px solid var(--bg);
545
+ box-sizing: content-box;
546
+ }
547
+ .tl-row[data-type="run.start"] .tl-node { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
548
+ .tl-row[data-type="run.end"] .tl-node { background: var(--text-2); }
549
+ .tl-row[data-type="run.end"][data-ok="false"] .tl-node { background: var(--err); }
550
+ .tl-row[data-type="error"] .tl-node { background: var(--err); box-shadow: 0 0 0 3px var(--err-soft); }
551
+ .tl-row[data-type="milestone"] .tl-node { background: var(--accent); }
552
+ .tl-row[data-type="progress"] .tl-node { width: 5px; height: 5px; margin-top: 6px; }
553
+
554
+ /* group runId connector — visually subtle indent */
555
+ .tl-row[data-grouped="true"] { padding-left: 0; }
556
+ .tl-row[data-grouped="true"] .tl-node { background: var(--text-3); }
557
+
558
+ .tl-content {
559
+ display: flex; align-items: baseline; gap: 8px;
560
+ padding-left: 8px;
561
+ min-width: 0;
562
+ }
563
+
564
+ .tl-badge {
565
+ font-family: var(--mono);
566
+ font-size: 10px;
567
+ letter-spacing: .04em;
568
+ padding: 1px 6px;
569
+ border-radius: 3px;
570
+ border: 1px solid var(--line);
571
+ color: var(--text-2);
572
+ background: var(--surface-1);
573
+ flex-shrink: 0;
574
+ display: inline-flex; align-items: center; gap: 4px;
575
+ }
576
+ .tl-badge .g { font-family: var(--mono); }
577
+
578
+ .tl-row[data-type="run.start"] .tl-badge { color: var(--accent); border-color: var(--accent-soft); background: var(--accent-soft); }
579
+ .tl-row[data-type="error"] .tl-badge { color: var(--err); border-color: var(--err-soft); background: var(--err-soft); }
580
+ .tl-row[data-type="milestone"] .tl-badge { color: var(--accent); border-color: var(--accent-soft); background: var(--accent-soft); }
581
+ .tl-row[data-type="run.end"][data-ok="true"] .tl-badge { color: var(--text); }
582
+ .tl-row[data-type="run.end"][data-ok="false"] .tl-badge { color: var(--err); border-color: var(--err-soft); background: var(--err-soft); }
583
+
584
+ .tl-msg {
585
+ flex: 1;
586
+ min-width: 0;
587
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
588
+ font-size: 12px;
589
+ color: var(--text);
590
+ }
591
+ .tl-row[data-type="progress"] .tl-msg { color: var(--text-2); }
592
+
593
+ .tl-msg .path,
594
+ .tl-msg .ident {
595
+ font-family: var(--mono);
596
+ font-size: 11px;
597
+ color: var(--text-2);
598
+ background: var(--surface-2);
599
+ padding: 1px 5px;
600
+ border-radius: 3px;
601
+ border: 1px solid var(--line);
602
+ }
603
+ .tl-msg .arrow { color: var(--text-3); margin: 0 4px; }
604
+
605
+ .tl-runid {
606
+ font-family: var(--mono);
607
+ font-size: 10px;
608
+ color: var(--text-4);
609
+ flex-shrink: 0;
610
+ }
611
+
612
+ /* ───────────────────────── empty state ───────────────────────── */
613
+ .empty {
614
+ margin: 32px 0;
615
+ padding: 48px 24px;
616
+ border: 1px dashed var(--line);
617
+ border-radius: var(--radius);
618
+ background:
619
+ radial-gradient(60% 40% at 50% 50%, var(--accent-soft) 0%, transparent 70%),
620
+ var(--surface-1);
621
+ display: flex; flex-direction: column; align-items: center; gap: 16px;
622
+ text-align: center;
623
+ }
624
+ .empty-viz {
625
+ width: 200px; height: 60px;
626
+ display: flex; align-items: center; justify-content: center;
627
+ gap: 4px;
628
+ }
629
+ .empty-viz .bar {
630
+ width: 4px;
631
+ background: var(--accent);
632
+ border-radius: 2px;
633
+ opacity: .5;
634
+ animation: heartbeat 1.6s var(--ease) infinite;
635
+ }
636
+ .empty-viz .bar:nth-child(1) { animation-delay: 0.0s; height: 12px; }
637
+ .empty-viz .bar:nth-child(2) { animation-delay: 0.05s; height: 18px; }
638
+ .empty-viz .bar:nth-child(3) { animation-delay: 0.10s; height: 28px; }
639
+ .empty-viz .bar:nth-child(4) { animation-delay: 0.15s; height: 40px; }
640
+ .empty-viz .bar:nth-child(5) { animation-delay: 0.20s; height: 52px; }
641
+ .empty-viz .bar:nth-child(6) { animation-delay: 0.25s; height: 40px; }
642
+ .empty-viz .bar:nth-child(7) { animation-delay: 0.30s; height: 28px; }
643
+ .empty-viz .bar:nth-child(8) { animation-delay: 0.35s; height: 18px; }
644
+ .empty-viz .bar:nth-child(9) { animation-delay: 0.40s; height: 12px; }
645
+ .empty-viz .bar:nth-child(10) { animation-delay: 0.45s; height: 22px; }
646
+ .empty-viz .bar:nth-child(11) { animation-delay: 0.50s; height: 36px; }
647
+ .empty-viz .bar:nth-child(12) { animation-delay: 0.55s; height: 24px; }
648
+ .empty-viz .bar:nth-child(13) { animation-delay: 0.60s; height: 14px; }
649
+
650
+ @keyframes heartbeat {
651
+ 0%, 100% { transform: scaleY(0.5); opacity: 0.3; }
652
+ 50% { transform: scaleY(1.0); opacity: 0.9; }
653
+ }
654
+
655
+ .empty h2 {
656
+ margin: 0;
657
+ font-size: 14px;
658
+ font-weight: 500;
659
+ color: var(--text);
660
+ letter-spacing: -.01em;
661
+ }
662
+ .empty p {
663
+ margin: 0;
664
+ font-size: 12px;
665
+ color: var(--text-3);
666
+ max-width: 320px;
667
+ }
668
+ .empty .hint {
669
+ font-family: var(--mono);
670
+ font-size: 10px;
671
+ color: var(--text-4);
672
+ display: flex; align-items: center; gap: 6px;
673
+ margin-top: 4px;
674
+ }
675
+ .empty .hint kbd {
676
+ font-family: var(--mono);
677
+ padding: 1px 5px;
678
+ border: 1px solid var(--line-strong);
679
+ border-bottom-width: 2px;
680
+ border-radius: 3px;
681
+ color: var(--text-2);
682
+ }
683
+
684
+ /* ───────────────────────── footer ───────────────────────── */
685
+ .footer {
686
+ padding: 16px 0;
687
+ margin-top: auto;
688
+ border-top: 1px solid var(--line);
689
+ display: flex; align-items: center; gap: 14px;
690
+ font-family: var(--mono);
691
+ font-size: 10px;
692
+ color: var(--text-4);
693
+ }
694
+ .footer .sep { color: var(--text-4); }
695
+ .footer .live { color: var(--accent); }
696
+
697
+ /* ───────────────────────── tweaks panel ───────────────────────── */
698
+ .tweaks {
699
+ position: fixed;
700
+ right: 16px; bottom: 16px;
701
+ width: 280px;
702
+ background: var(--surface-2);
703
+ border: 1px solid var(--line-strong);
704
+ border-radius: 12px;
705
+ box-shadow: 0 24px 60px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.02);
706
+ z-index: 50;
707
+ overflow: hidden;
708
+ display: none;
709
+ animation: tweaks-in .25s var(--ease);
710
+ }
711
+ .tweaks.open { display: block; }
712
+ @keyframes tweaks-in {
713
+ from { opacity: 0; transform: translateY(8px); }
714
+ to { opacity: 1; transform: none; }
715
+ }
716
+ .tweaks h3 {
717
+ margin: 0;
718
+ padding: 14px 14px 10px;
719
+ font-family: var(--mono);
720
+ font-size: 10px;
721
+ text-transform: uppercase;
722
+ letter-spacing: .14em;
723
+ color: var(--text-3);
724
+ display: flex; align-items: center; gap: 8px;
725
+ border-bottom: 1px solid var(--line);
726
+ }
727
+ .tweaks h3 .close {
728
+ margin-left: auto;
729
+ width: 18px; height: 18px;
730
+ display: grid; place-items: center;
731
+ border-radius: 4px;
732
+ color: var(--text-3);
733
+ }
734
+ .tweaks h3 .close:hover { background: var(--surface-3); color: var(--text); }
735
+ .tweaks-body { padding: 8px 14px 14px; display: flex; flex-direction: column; gap: 14px; }
736
+ .tw-group { display: flex; flex-direction: column; gap: 6px; }
737
+ .tw-label {
738
+ font-family: var(--mono);
739
+ font-size: 10px;
740
+ text-transform: uppercase;
741
+ letter-spacing: .08em;
742
+ color: var(--text-3);
743
+ }
744
+ .seg {
745
+ display: grid;
746
+ grid-auto-flow: column;
747
+ grid-auto-columns: 1fr;
748
+ gap: 2px;
749
+ padding: 2px;
750
+ background: var(--surface-3);
751
+ border-radius: 6px;
752
+ border: 1px solid var(--line);
753
+ }
754
+ .seg button {
755
+ padding: 5px 8px;
756
+ font-size: 11px;
757
+ color: var(--text-2);
758
+ border-radius: 4px;
759
+ transition: all .12s var(--ease);
760
+ font-family: var(--sans);
761
+ }
762
+ .seg button[aria-pressed="true"] {
763
+ background: var(--surface-1);
764
+ color: var(--text);
765
+ box-shadow: 0 1px 0 rgba(255,255,255,.04), 0 1px 4px rgba(0,0,0,.4);
766
+ }
767
+ .seg button:not([aria-pressed="true"]):hover { color: var(--text); }
768
+
769
+ .swatches {
770
+ display: grid;
771
+ grid-template-columns: repeat(6, 1fr);
772
+ gap: 6px;
773
+ }
774
+ .swatches button {
775
+ height: 22px;
776
+ border-radius: 4px;
777
+ border: 1px solid var(--line);
778
+ position: relative;
779
+ }
780
+ .swatches button[aria-pressed="true"] {
781
+ border-color: var(--text);
782
+ outline: 2px solid var(--text);
783
+ outline-offset: -3px;
784
+ }
785
+
786
+ .tw-actions {
787
+ display: flex; gap: 6px;
788
+ padding-top: 4px;
789
+ border-top: 1px solid var(--line);
790
+ margin-top: 4px;
791
+ }
792
+ .tw-actions button {
793
+ flex: 1;
794
+ padding: 6px;
795
+ font-size: 11px;
796
+ border-radius: 6px;
797
+ border: 1px solid var(--line);
798
+ color: var(--text-2);
799
+ background: var(--surface-1);
800
+ transition: all .12s var(--ease);
801
+ font-family: var(--mono);
802
+ }
803
+ .tw-actions button:hover { color: var(--text); background: var(--surface-3); }
804
+
805
+ /* density-driven log padding handled via --pad-tight above */
806
+
807
+ /* ───────────────────────── reduced motion ───────────────────────── */
808
+ @media (prefers-reduced-motion: reduce) {
809
+ *, *::before, *::after {
810
+ animation-duration: .01ms !important;
811
+ animation-iteration-count: 1 !important;
812
+ transition-duration: .01ms !important;
813
+ }
814
+ }
448
815
  </style>
449
816
  </head>
450
817
  <body>
451
- <header>
452
- <h1>kit-mcp sidecar</h1>
453
- <span class="meta" id="meta-port">porta —</span>
454
- <span class="grow"></span>
455
- <span class="conn-status" id="conn" data-state="CONNECTING"><span class="dot"></span><span id="conn-text">CONECTANDO</span></span>
818
+
819
+ <div class="app">
820
+
821
+ <!-- HEADER -->
822
+ <header class="header">
823
+ <div class="logo" aria-hidden="true">
824
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
825
+ <path d="M4 7h16M4 12h10M4 17h16"/>
826
+ </svg>
827
+ </div>
828
+ <div class="brand">
829
+ <div class="brand-row">
830
+ <span class="brand-name">kit-mcp</span>
831
+ <span class="brand-meta" id="brand-meta">sidecar</span>
832
+ </div>
833
+ <div class="brand-sub">127.0.0.1:7100</div>
834
+ </div>
835
+ <div class="spacer"></div>
836
+ <div class="conn" id="conn" data-state="on" title="Conexão SSE">
837
+ <span class="dot"></span>
838
+ <span id="conn-label">conectado</span>
839
+ </div>
456
840
  </header>
457
841
 
842
+ <!-- TOOLBAR -->
458
843
  <div class="toolbar">
459
- <input type="search" id="search" placeholder="filtrar por nome ou conteúdo…" autocomplete="off">
460
- <fieldset class="filters" id="type-filters" aria-label="Tipos de evento">
461
- <!-- populated from EVENT_TYPES at runtime -->
462
- </fieldset>
463
- <button id="pause-btn" aria-pressed="false">⏸ pausar</button>
464
- <button id="autoscroll-btn" aria-pressed="true">↧ rolagem auto</button>
465
- <button id="clear-btn" title="Limpa o que está visível (histórico no servidor preservado)">limpar tela</button>
844
+ <label class="search">
845
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
846
+ <circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>
847
+ </svg>
848
+ <input id="q" type="text" placeholder="buscar tool, runId, caminho…" autocomplete="off" spellcheck="false" />
849
+ <kbd>/</kbd>
850
+ </label>
851
+ <div class="filter-wrap">
852
+ <button class="iconbtn" id="filter-btn" aria-pressed="false" aria-label="Filtros" title="Filtros">
853
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
854
+ <path d="M3 6h18M6 12h12M10 18h4"/>
855
+ </svg>
856
+ </button>
857
+ <div class="filter-pop" id="filter-pop" role="menu">
858
+ <div class="fp-title">Tipos</div>
859
+ <div class="fp-row" data-on="true" data-filter="run.start"><span class="fp-check">✓</span><span class="fp-glyph">▸</span>Iniciado</div>
860
+ <div class="fp-row" data-on="true" data-filter="progress"><span class="fp-check">✓</span><span class="fp-glyph">⟳</span>Em andamento</div>
861
+ <div class="fp-row" data-on="true" data-filter="milestone"><span class="fp-check">✓</span><span class="fp-glyph">★</span>Marco</div>
862
+ <div class="fp-row" data-on="true" data-filter="run.end"><span class="fp-check">✓</span><span class="fp-glyph">✓</span>Finalizado</div>
863
+ <div class="fp-row" data-on="true" data-filter="error"><span class="fp-check">✓</span><span class="fp-glyph">✕</span>Erro</div>
864
+ </div>
865
+ </div>
866
+ <button class="iconbtn" id="pause-btn" aria-pressed="false" aria-label="Pausar stream" title="Pausar stream">
867
+ <svg id="pause-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
868
+ <rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/>
869
+ </svg>
870
+ </button>
871
+ <button class="iconbtn" id="tweaks-btn" aria-pressed="false" aria-label="Tweaks" title="Tweaks">
872
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
873
+ <circle cx="12" cy="12" r="3"/>
874
+ <path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"/>
875
+ </svg>
876
+ </button>
466
877
  </div>
467
878
 
879
+ <!-- ACTIVE + LOG -->
468
880
  <main>
469
- <div class="banner" id="shutdown-banner" hidden>
470
- <strong>Sidecar encerrou.</strong> Recarregue depois de <code>kit ui start</code>.
881
+ <section id="active-region" class="active-region" data-count="0" aria-live="polite"></section>
882
+
883
+ <div class="section-head" id="log-head" hidden>
884
+ <span class="section-title">Linha do tempo</span>
885
+ <span class="section-count" id="log-count">0 eventos</span>
471
886
  </div>
472
887
 
473
- <section class="runs" id="active-runs" hidden aria-label="Em execução agora">
474
- <div class="runs-header">
475
- <span>Em execução agora</span>
476
- <span class="count" id="active-runs-count">0</span>
888
+ <section id="timeline" class="timeline"></section>
889
+
890
+ <section id="empty" class="empty">
891
+ <div class="empty-viz" aria-hidden="true">
892
+ <span class="bar"></span><span class="bar"></span><span class="bar"></span>
893
+ <span class="bar"></span><span class="bar"></span><span class="bar"></span>
894
+ <span class="bar"></span><span class="bar"></span><span class="bar"></span>
895
+ <span class="bar"></span><span class="bar"></span><span class="bar"></span>
896
+ <span class="bar"></span>
897
+ </div>
898
+ <h2>Aguardando o primeiro evento</h2>
899
+ <p>O sidecar está escutando em <span style="font-family:var(--mono);color:var(--text-2)">/events</span>. Rode qualquer comando do kit-mcp e ele aparece aqui ao vivo.</p>
900
+ <div class="hint">
901
+ Tente <kbd>kit sync</kbd> · <kbd>kit gates</kbd>
477
902
  </div>
478
- <div id="active-runs-list"></div>
479
903
  </section>
480
-
481
- <ul class="ev-list" id="events" hidden></ul>
482
- <div class="empty" id="empty">
483
- <strong>Aguardando primeiro evento…</strong>
484
- Rode <code>kit sync install</code>, <code>kit reverse-sync apply</code>, ou
485
- chame uma ferramenta MCP com <code>autoSpawn: true</code> em outra janela.
486
- </div>
487
904
  </main>
488
905
 
489
- <div class="footer">
490
- <span id="footer-events">eventos: 0</span>
491
- <span id="footer-paused" hidden>pausado: 0 em fila</span>
492
- <span id="footer-source">fonte: ao vivo</span>
493
- </div>
494
-
495
- <script>
496
- // src/ui/static/index.html — vanilla DOM client for the kit-mcp sidecar.
497
- // Connects to /events (SSE) and hydrates from /state on load.
498
- // No build step. No deps.
499
-
500
- 'use strict';
501
-
502
- const EVENT_TYPES = ['run.start', 'run.end', 'tool_invocation', 'progress', 'milestone', 'error', 'shutdown'];
503
- const RING_DISPLAY_MAX = 500;
504
-
505
- // ------- Humanization (PT-BR friendly labels) ------------------------
506
- // Maps raw technical event types and tool ids to short human-readable
507
- // labels. The technical values stay in the data attributes (so CSS colors
508
- // and filters still key off the raw type) — only the display text changes.
509
-
510
- const EVENT_TYPE_LABEL = {
511
- 'run.start': 'Iniciado',
512
- 'run.end': 'Finalizado',
513
- 'tool_invocation': 'Comando',
514
- 'progress': 'Em andamento',
515
- 'milestone': 'Marco',
516
- 'error': 'Erro',
517
- 'shutdown': 'Desligado',
518
- };
519
-
520
- const TOOL_LABEL = {
521
- 'sync.install': 'Sincronizando kit',
522
- 'sync.watch': 'Vigiando kit (watch)',
523
- 'reverse-sync.apply': 'Importando edições do IDE',
524
- 'gates.run': 'Executando gate',
525
- 'sidecar': 'Servidor sidecar',
526
- };
527
-
528
- const STATUS_LABEL = {
529
- running: 'em execução',
530
- done: 'concluído',
531
- error: 'erro',
532
- };
906
+ <!-- FOOTER -->
907
+ <footer class="footer">
908
+ <span id="evt-count">0 eventos</span>
909
+ <span class="sep">·</span>
910
+ <span>fonte: <span class="live" id="src-label">ao vivo</span></span>
911
+ <span class="sep">·</span>
912
+ <span id="last-seen">aguardando…</span>
913
+ </footer>
914
+
915
+ </div>
916
+
917
+ <!-- TWEAKS -->
918
+ <aside class="tweaks" id="tweaks" role="dialog" aria-label="Tweaks">
919
+ <h3>
920
+ Tweaks
921
+ <button class="close" id="tweaks-close" aria-label="Fechar">
922
+ <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>
923
+ </button>
924
+ </h3>
925
+ <div class="tweaks-body">
926
+ <div class="tw-group">
927
+ <span class="tw-label">Acento</span>
928
+ <div class="swatches" id="tw-accent">
929
+ <button data-h="130" style="background:oklch(82% 0.18 130)" aria-label="Lima"></button>
930
+ <button data-h="220" style="background:oklch(78% 0.16 220)" aria-label="Azul"></button>
931
+ <button data-h="290" style="background:oklch(78% 0.18 290)" aria-label="Roxo"></button>
932
+ <button data-h="20" style="background:oklch(78% 0.18 20)" aria-label="Laranja"></button>
933
+ <button data-h="340" style="background:oklch(78% 0.18 340)" aria-label="Magenta"></button>
934
+ <button data-h="180" style="background:oklch(80% 0.14 180)" aria-label="Ciano"></button>
935
+ </div>
936
+ </div>
533
937
 
534
- const CONN_LABEL = {
535
- CONNECTING: 'CONECTANDO',
536
- OPEN: 'CONECTADO',
537
- CLOSED: 'DESCONECTADO',
538
- };
938
+ <div class="tw-group">
939
+ <span class="tw-label">Densidade</span>
940
+ <div class="seg" id="tw-density">
941
+ <button data-v="compact">Compacta</button>
942
+ <button data-v="normal" aria-pressed="true">Normal</button>
943
+ <button data-v="comfy">Confortável</button>
944
+ </div>
945
+ </div>
539
946
 
540
- const PATH_VERB = {
541
- reading: 'lendo',
542
- writing: 'criando',
543
- projecting: 'projetando',
544
- merging: 'mesclando',
545
- copying: 'copiando',
546
- deleting: 'removendo',
547
- creating: 'criando',
548
- updating: 'atualizando',
549
- syncing: 'sincronizando',
550
- 'sync': 'sincronizando',
551
- applying: 'aplicando',
552
- fetching: 'buscando',
553
- };
947
+ <div class="tw-group">
948
+ <span class="tw-label">Movimento</span>
949
+ <div class="seg" id="tw-motion">
950
+ <button data-v="subtle">Sutil</button>
951
+ <button data-v="medium" aria-pressed="true">Médio</button>
952
+ <button data-v="rich">Rico</button>
953
+ </div>
954
+ </div>
554
955
 
555
- function humanizeEventType(t) { return EVENT_TYPE_LABEL[t] || t; }
556
- function humanizeTool(t) { return TOOL_LABEL[t] || t; }
557
- function humanizeStatus(s) { return STATUS_LABEL[s] || s; }
558
-
559
- // Turn a path / file reference inside a label into a friendlier description.
560
- // Examples:
561
- // .claude/agents/planner.md → agente planner
562
- // kit/commands/novo-marco.md → comando novo-marco
563
- // kit/skills/limpeza/SKILL.md → skill limpeza
564
- // .claude/framework/templates/codebase/x.md → template framework
565
- // CLAUDE.md → manifesto CLAUDE.md
566
- function humanizePath(p) {
567
- if (typeof p !== 'string' || !p.length) return '';
568
- const norm = p.replace(/\\/g, '/');
569
-
570
- let m;
571
- if ((m = norm.match(/(?:\.claude|kit)\/agents\/([^/]+)\.md$/))) return `agente ${m[1]}`;
572
- if ((m = norm.match(/(?:\.claude|kit)\/commands\/([^/]+)\.md$/))) return `comando ${m[1]}`;
573
- if ((m = norm.match(/(?:\.claude|kit)\/skills\/([^/]+)/))) return `skill ${m[1]}`;
574
- if ((m = norm.match(/(?:\.claude|kit)\/framework\//))) return 'framework';
575
- if ((m = norm.match(/(?:\.claude|kit)\/hooks\//))) return 'hooks';
576
- if (norm === 'CLAUDE.md' || norm.endsWith('/CLAUDE.md')) return 'manifesto CLAUDE.md';
577
- if ((m = norm.match(/(?:\.claude|kit)\/([^/]+)\/([^/]+\.md)$/))) return `${m[1]} ${m[2].replace(/\.md$/, '')}`;
578
- return norm; // fall back to the raw path if no rule matched
579
- }
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>
580
965
 
581
- // Translate a raw progress label like "writing .claude/agents/planner.md"
582
- // into "criando agente planner". The function is best-effort: if no rule
583
- // matches, returns the original label so we never lose information.
584
- function humanizeLabel(raw) {
585
- if (typeof raw !== 'string' || !raw.length) return raw;
586
-
587
- // verb + path (single token after verb)
588
- const verbMatch = raw.match(/^(reading|writing|projecting|merging|copying|deleting|creating|updating|syncing|sync|applying|fetching)\s+(.+?)(?:\s+\(.+\))?$/i);
589
- if (verbMatch) {
590
- const verb = PATH_VERB[verbMatch[1].toLowerCase()] || verbMatch[1];
591
- const target = humanizePath(verbMatch[2].trim());
592
- return `${verb} ${target}`;
593
- }
966
+ <div class="tw-actions">
967
+ <button id="tw-replay">▸ replay</button>
968
+ <button id="tw-clear">limpar</button>
969
+ </div>
970
+ </div>
971
+ </aside>
972
+
973
+ <!-- SVG sprite (in-doc, no external) -->
974
+ <svg width="0" height="0" style="position:absolute" aria-hidden="true">
975
+ <defs>
976
+ <symbol id="i-sync" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
977
+ <path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/>
978
+ <path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 21v-5h5"/>
979
+ </symbol>
980
+ <symbol id="i-reverse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
981
+ <path d="M7 7h10l-3-3"/><path d="M17 17H7l3 3"/>
982
+ </symbol>
983
+ <symbol id="i-gates" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
984
+ <rect x="4" y="4" width="16" height="16" rx="2"/><path d="M4 10h16M10 4v16"/>
985
+ </symbol>
986
+ <symbol id="i-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
987
+ <path d="M12 3a9 9 0 1 0 9 9" />
988
+ </symbol>
989
+ <symbol id="i-bolt" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
990
+ <path d="M13 2 4 14h7l-1 8 9-12h-7l1-8Z"/>
991
+ </symbol>
992
+ </defs>
993
+ </svg>
594
994
 
595
- // bare path
596
- if (/^[^\s]+\.md$/.test(raw) || /^(?:\.claude|kit)\//.test(raw)) {
597
- return humanizePath(raw);
995
+ <script>
996
+ /* ──────────────────────────────────────────────────────────
997
+ kit-mcp sidecar — prototype
998
+ This is a faithful mock of what the production HTML will do.
999
+ In production, replace the MockSource with a real EventSource('/events').
1000
+ ────────────────────────────────────────────────────────── */
1001
+
1002
+ /* ---------- humanize helpers (preserved API) ---------- */
1003
+ const TYPE_LABELS = {
1004
+ "run.start": "INICIADO",
1005
+ "run.end": "FINALIZADO",
1006
+ "tool_invocation":"INVOCADO",
1007
+ "progress": "EM ANDAMENTO",
1008
+ "milestone": "MARCO",
1009
+ "error": "ERRO",
1010
+ "shutdown": "ENCERRADO",
1011
+ };
1012
+ const TOOL_LABELS = {
1013
+ "sync.install": "Sincronizando kit",
1014
+ "sync.preview": "Prévia de sincronização",
1015
+ "reverse.scan": "Escaneando agentes",
1016
+ "reverse.merge": "Mesclando alterações",
1017
+ "gates.run": "Executando gates",
1018
+ "gates.lint": "Lint dos agentes",
1019
+ "kit.list": "Listando kit",
1020
+ };
1021
+ const TOOL_FAMILIES = {
1022
+ "sync.install": "sync",
1023
+ "sync.preview": "sync",
1024
+ "reverse.scan": "reverse",
1025
+ "reverse.merge": "reverse",
1026
+ "gates.run": "gates",
1027
+ "gates.lint": "gates",
1028
+ "kit.list": "sync",
1029
+ };
1030
+ function humanizeEventType(t) { return TYPE_LABELS[t] || t.toUpperCase(); }
1031
+ function humanizeTool(t) { return TOOL_LABELS[t] || t; }
1032
+ function humanizePath(p) {
1033
+ if (!p) return "";
1034
+ // collapse leading paths, keep last 2 segments
1035
+ const parts = p.split("/").filter(Boolean);
1036
+ if (parts.length <= 2) return p;
1037
+ return "…/" + parts.slice(-2).join("/");
1038
+ }
1039
+
1040
+ /* ---------- relative time ---------- */
1041
+ function relTime(ms) {
1042
+ const s = Math.floor(ms / 1000);
1043
+ if (s < 5) return "agora";
1044
+ if (s < 60) return `há ${s}s`;
1045
+ const m = Math.floor(s / 60);
1046
+ if (m < 60) return `há ${m}m`;
1047
+ const h = Math.floor(m / 60);
1048
+ if (h < 24) return `há ${h}h`;
1049
+ const d = Math.floor(h / 24);
1050
+ return `há ${d}d`;
1051
+ }
1052
+ function clockTime(ts) {
1053
+ const d = new Date(ts);
1054
+ return `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}:${String(d.getSeconds()).padStart(2,"0")}`;
1055
+ }
1056
+
1057
+ /* ---------- state ---------- */
1058
+ const state = {
1059
+ events: [], // newest first
1060
+ runs: new Map(), // runId → { startTs, tool, lastProgress, lastLabel, ended, ok, current, total }
1061
+ filterText: "",
1062
+ // tool_invocation + shutdown não têm chip de filtro, mas devem aparecer por default
1063
+ filters: new Set(["run.start","progress","milestone","run.end","error","tool_invocation","shutdown"]),
1064
+ paused: false,
1065
+ };
1066
+
1067
+ /* ---------- DOM refs ---------- */
1068
+ const $ = (s) => document.querySelector(s);
1069
+ const els = {
1070
+ active: $("#active-region"),
1071
+ timeline: $("#timeline"),
1072
+ empty: $("#empty"),
1073
+ evtCount: $("#evt-count"),
1074
+ logCount: $("#log-count"),
1075
+ logHead: $("#log-head"),
1076
+ lastSeen: $("#last-seen"),
1077
+ conn: $("#conn"),
1078
+ connLabel: $("#conn-label"),
1079
+ q: $("#q"),
1080
+ pauseBtn: $("#pause-btn"),
1081
+ pauseIcon: $("#pause-icon"),
1082
+ filterBtn: $("#filter-btn"),
1083
+ filterPop: $("#filter-pop"),
1084
+ tweaksBtn: $("#tweaks-btn"),
1085
+ tweaks: $("#tweaks"),
1086
+ tweaksClose: $("#tweaks-close"),
1087
+ srcLabel: $("#src-label"),
1088
+ };
1089
+
1090
+ /* ---------- ingest one event ---------- */
1091
+ function ingest(evt) {
1092
+ if (state.paused) return;
1093
+ state.events.unshift(evt);
1094
+ if (state.events.length > 200) state.events.length = 200;
1095
+
1096
+ // run tracking
1097
+ if (evt.runId) {
1098
+ let run = state.runs.get(evt.runId);
1099
+ 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 };
1101
+ state.runs.set(evt.runId, run);
598
1102
  }
599
-
600
- // pure technical action, no path: leave as-is
601
- return raw;
602
- }
603
-
604
- const $ = (id) => document.getElementById(id);
605
- const dom = {
606
- conn: $('conn'),
607
- connText: $('conn-text'),
608
- metaPort: $('meta-port'),
609
- list: $('events'),
610
- empty: $('empty'),
611
- banner: $('shutdown-banner'),
612
- pauseBtn: $('pause-btn'),
613
- autoscrollBtn: $('autoscroll-btn'),
614
- clearBtn: $('clear-btn'),
615
- search: $('search'),
616
- typeFilters: $('type-filters'),
617
- footerEvents: $('footer-events'),
618
- footerPaused: $('footer-paused'),
619
- footerSource: $('footer-source'),
620
- activeRuns: $('active-runs'),
621
- activeRunsList: $('active-runs-list'),
622
- activeRunsCount: $('active-runs-count'),
623
- };
624
-
625
- const state = {
626
- events: [], // currently rendered events (buffered while paused)
627
- pausedBuffer: [], // events captured while paused
628
- paused: false,
629
- autoscroll: true,
630
- typeFilter: new Set(EVENT_TYPES), // all enabled
631
- search: '',
632
- closedAt: null,
633
- };
634
-
635
- // ------- DOM helpers --------------------------------------------------
636
-
637
- function el(tag, props = {}, kids = []) {
638
- const e = document.createElement(tag);
639
- for (const [k, v] of Object.entries(props)) {
640
- if (k === 'class') e.className = v;
641
- else if (k === 'data') for (const [dk, dv] of Object.entries(v)) e.dataset[dk] = dv;
642
- else if (k.startsWith('on')) e.addEventListener(k.slice(2).toLowerCase(), v);
643
- else if (k === 'text') e.textContent = v;
644
- else if (v !== undefined && v !== null) e.setAttribute(k, v);
1103
+ run.lastTs = evt.ts;
1104
+ if (evt.type === "run.start") {
1105
+ run.tool = evt.payload?.tool;
1106
+ run.startTs = evt.ts;
1107
+ } else if (evt.type === "progress") {
1108
+ if (typeof evt.payload?.percent === "number") run.lastProgress = evt.payload.percent;
1109
+ if (evt.payload?.label) run.lastLabel = evt.payload.label;
1110
+ if (evt.payload?.current) run.current = evt.payload.current;
1111
+ if (evt.payload?.total) run.total = evt.payload.total;
1112
+ } else if (evt.type === "run.end") {
1113
+ run.ended = true;
1114
+ run.ok = !!evt.payload?.ok;
1115
+ run.lastProgress = run.ok ? 100 : run.lastProgress;
1116
+ } else if (evt.type === "error") {
1117
+ // don't end the run on error event alone; matches server semantics
645
1118
  }
646
- for (const k of kids) if (k) e.appendChild(typeof k === 'string' ? document.createTextNode(k) : k);
647
- return e;
648
1119
  }
649
1120
 
650
- function fmtTime(ts) {
651
- const d = new Date(ts);
652
- const hh = String(d.getHours()).padStart(2, '0');
653
- const mm = String(d.getMinutes()).padStart(2, '0');
654
- const ss = String(d.getSeconds()).padStart(2, '0');
655
- return `${hh}:${mm}:${ss}`;
656
- }
657
-
658
- function eventLabel(evt) {
659
- // Best-effort short label from payload; humanizes paths and tool ids;
660
- // falls back to the humanized event type when payload has nothing useful.
661
- const p = evt.payload;
662
- if (!p || typeof p !== 'object') return humanizeEventType(evt.type);
663
- if (typeof p.label === 'string') return humanizeLabel(p.label);
664
- if (typeof p.name === 'string') return p.name;
665
- if (typeof p.tool === 'string') return humanizeTool(p.tool);
666
- if (typeof p.percent === 'number') return `${p.percent}%${p.kind ? ' · ' + p.kind : ''}`;
667
- if (typeof p.message === 'string') return p.message;
668
- if (typeof p.reason === 'string') return p.reason;
669
- return humanizeEventType(evt.type);
670
- }
1121
+ render();
1122
+ els.lastSeen.textContent = "último: " + clockTime(evt.ts);
1123
+ }
1124
+
1125
+ /* ---------- render ---------- */
1126
+ function passesFilter(evt) {
1127
+ if (!state.filters.has(evt.type)) return false;
1128
+ if (!state.filterText) return true;
1129
+ const q = state.filterText.toLowerCase();
1130
+ const blob = JSON.stringify(evt).toLowerCase();
1131
+ return blob.includes(q);
1132
+ }
1133
+
1134
+ function render() {
1135
+ renderActive();
1136
+ renderTimeline();
1137
+ const total = state.events.length;
1138
+ els.evtCount.textContent = `${total} evento${total === 1 ? "" : "s"}`;
1139
+ els.logCount.textContent = `${total} evento${total === 1 ? "" : "s"}`;
1140
+ }
1141
+
1142
+ function renderActive() {
1143
+ const active = [...state.runs.values()].filter((r) => !r.ended).sort((a, b) => b.startTs - a.startTs);
1144
+ els.active.dataset.count = active.length;
1145
+
1146
+ // remove cards that are no longer active
1147
+ els.active.querySelectorAll(".run-card").forEach((card) => {
1148
+ const id = card.dataset.runid;
1149
+ if (!active.find((r) => r.runId === id)) card.remove();
1150
+ });
671
1151
 
672
- function eventMeta(evt) {
673
- const p = evt.payload;
674
- if (evt.type === 'progress' && p && typeof p.total === 'number' && typeof p.current === 'number') {
675
- return `${p.current}/${p.total}`;
1152
+ // upsert each active run
1153
+ active.forEach((run) => {
1154
+ let card = els.active.querySelector(`[data-runid="${run.runId}"]`);
1155
+ if (!card) {
1156
+ const wrap = document.createElement("div");
1157
+ wrap.innerHTML = activeCardHtml(run);
1158
+ card = wrap.firstElementChild;
1159
+ card.classList.add("enter");
1160
+ card.addEventListener("animationend", () => card.classList.remove("enter"), { once: true });
1161
+ setTimeout(() => card.classList.remove("enter"), 600);
1162
+ els.active.appendChild(card);
676
1163
  }
677
- if (evt.runId) return evt.runId.slice(0, 6);
678
- return '';
679
- }
1164
+ updateActiveCard(card, run);
1165
+ });
1166
+ }
1167
+
1168
+ function updateActiveCard(card, run) {
1169
+ const family = TOOL_FAMILIES[run.tool] || "sync";
1170
+ const percent = Math.max(0, Math.min(100, Math.round(run.lastProgress)));
1171
+ const elapsed = Date.now() - run.startTs;
1172
+ const longRunning = elapsed > 30_000;
1173
+ const stepLabel = humanizePath(run.lastLabel) || "iniciando…";
1174
+ const stepCount = run.total ? `${run.current}/${run.total}` : "";
1175
+
1176
+ const set = (sel, fn) => { const n = card.querySelector(sel); if (n) fn(n); };
1177
+ 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 = humanizeTool(run.tool || "—"); });
1180
+ set(".rc-bar-fill", (n) => { n.style.width = percent + "%"; });
1181
+ set(".rc-pct", (n) => { n.textContent = percent + "%"; });
1182
+ set(".rc-step-text", (n) => { if (n.textContent !== stepLabel) n.textContent = stepLabel; });
1183
+ set(".rc-step-count", (n) => { n.textContent = stepCount; });
1184
+ set(".rc-elapsed-val", (n) => { n.textContent = formatElapsed(elapsed); });
1185
+ set(".rc-elapsed .em", (n) => n.classList.toggle("warn", longRunning));
1186
+ set(".rc-runid", (n) => { n.textContent = "id " + run.runId.slice(0, 8); });
1187
+ }
1188
+
1189
+ function activeCardHtml(run) {
1190
+ const family = TOOL_FAMILIES[run.tool] || "sync";
1191
+ const iconHref = `#i-${family}`;
1192
+ const title = humanizeTool(run.tool || "—");
1193
+ const stepLabel = humanizePath(run.lastLabel) || "iniciando…";
1194
+ const percent = Math.max(0, Math.min(100, Math.round(run.lastProgress)));
1195
+ const elapsed = Date.now() - run.startTs;
1196
+ const longRunning = elapsed > 30_000;
1197
+ const stepCount = run.total ? `${run.current}/${run.total}` : "";
1198
+ return `
1199
+ <article class="run-card" data-runid="${run.runId}">
1200
+ <div class="rc-head">
1201
+ <div class="rc-icon" data-tool="${family}"><svg><use href="${iconHref}"/></svg></div>
1202
+ <div class="rc-title-block">
1203
+ <div class="rc-tool">${run.tool || "—"}</div>
1204
+ <div class="rc-title">${title}</div>
1205
+ </div>
1206
+ <div class="rc-elapsed">
1207
+ <span class="em ${longRunning ? "warn" : ""}"><span class="rc-elapsed-val">${formatElapsed(elapsed)}</span></span>
1208
+ <small>decorrido</small>
1209
+ </div>
1210
+ </div>
680
1211
 
681
- function renderEventRow(evt) {
682
- const time = el('div', { class: 'ev-time', text: fmtTime(evt.ts) });
683
- // data-type stays raw so CSS color rules continue to apply; display text is humanized.
684
- const badge = el('span', { class: 'ev-badge', data: { type: evt.type }, text: humanizeEventType(evt.type) });
685
- const summary = el('summary', {}, [
686
- el('span', { class: 'label', text: eventLabel(evt) }),
687
- el('span', { class: 'meta', text: eventMeta(evt) }),
688
- ]);
689
- const pre = el('pre', { text: JSON.stringify(evt.payload ?? null, null, 2) });
690
- const details = el('details', {}, [summary, pre]);
691
- const body = el('div', { class: 'ev-body' }, [details]);
692
- return el('li', { class: 'ev-row', data: { type: evt.type, label: eventLabel(evt).toLowerCase() } }, [time, badge, body]);
693
- }
1212
+ <div class="rc-progress">
1213
+ <div class="rc-bar"><div class="rc-bar-fill" style="width:${percent}%"></div></div>
1214
+ <div class="rc-pct">${percent}%</div>
1215
+ </div>
694
1216
 
695
- function shouldShow(evt) {
696
- if (!state.typeFilter.has(evt.type)) return false;
697
- if (state.search) {
698
- const haystack = (evt.type + ' ' + eventLabel(evt) + ' ' + JSON.stringify(evt.payload ?? '')).toLowerCase();
699
- if (!haystack.includes(state.search.toLowerCase())) return false;
700
- }
701
- return true;
702
- }
1217
+ <div class="rc-step">
1218
+ <span class="glyph"><svg><use href="#i-spin"/></svg></span>
1219
+ <span class="rc-step-text">${escapeHtml(stepLabel)}</span>
1220
+ ${stepCount ? `<span class="rc-step-count">${stepCount}</span>` : ""}
1221
+ </div>
703
1222
 
704
- function applyFilter() {
705
- const rows = dom.list.children;
706
- let visible = 0;
707
- for (let i = 0; i < rows.length; i += 1) {
708
- const row = rows[i];
709
- const evt = state.events[i];
710
- const show = evt && shouldShow(evt);
711
- row.style.display = show ? '' : 'none';
712
- if (show) visible += 1;
713
- }
714
- dom.empty.hidden = visible > 0 || state.events.length > 0;
715
- dom.list.hidden = visible === 0 && state.events.length > 0 ? false : visible === 0;
716
- if (state.events.length === 0) {
717
- dom.empty.hidden = false;
718
- dom.list.hidden = true;
1223
+ <div class="rc-foot">
1224
+ <span class="rc-runid">id ${run.runId.slice(0, 8)}</span>
1225
+ <span class="sep">·</span>
1226
+ <span>${run.tool || ""}</span>
1227
+ </div>
1228
+ </article>
1229
+ `;
1230
+ }
1231
+
1232
+ function formatElapsed(ms) {
1233
+ const s = Math.floor(ms / 1000);
1234
+ if (s < 60) return `${s}s`;
1235
+ const m = Math.floor(s / 60);
1236
+ const r = s % 60;
1237
+ return `${m}m ${String(r).padStart(2,"0")}s`;
1238
+ }
1239
+
1240
+ function renderTimeline() {
1241
+ const visible = state.events.filter(passesFilter);
1242
+ if (state.events.length === 0) {
1243
+ els.empty.style.display = "";
1244
+ els.timeline.innerHTML = "";
1245
+ els.logHead.hidden = true;
1246
+ return;
1247
+ }
1248
+ els.empty.style.display = "none";
1249
+ els.logHead.hidden = false;
1250
+
1251
+ // diff by event ts+type+runId — only animate truly new rows
1252
+ const keyOf = (e) => `${e.ts}|${e.type}|${e.runId || ""}`;
1253
+ const seen = state._seenKeys || (state._seenKeys = new Set());
1254
+ const existing = new Map();
1255
+ els.timeline.querySelectorAll(".tl-row").forEach((r) => existing.set(r.dataset.key, r));
1256
+
1257
+ // Build a desired ordered list. Insert new rows at their correct position
1258
+ // WITHOUT detaching existing rows — that would retrigger CSS animations.
1259
+ let prevNode = null;
1260
+ visible.forEach((evt, idx) => {
1261
+ const key = keyOf(evt);
1262
+ let row = existing.get(key);
1263
+ if (row) {
1264
+ existing.delete(key);
1265
+ // reorder only if not already in the right place
1266
+ const expectedNext = prevNode ? prevNode.nextSibling : els.timeline.firstChild;
1267
+ if (row !== expectedNext) {
1268
+ els.timeline.insertBefore(row, expectedNext);
1269
+ }
719
1270
  } else {
720
- dom.empty.hidden = true;
721
- dom.list.hidden = false;
1271
+ const wrap = document.createElement("div");
1272
+ wrap.innerHTML = rowHtml(evt, idx, visible[idx - 1]);
1273
+ row = wrap.firstElementChild;
1274
+ row.dataset.key = key;
1275
+ if (!seen.has(key)) {
1276
+ row.classList.add("enter", "is-new");
1277
+ seen.add(key);
1278
+ // strip the one-shot class once the animation completes,
1279
+ // so reattachment / reorder won't restart it.
1280
+ row.addEventListener("animationend", () => row.classList.remove("enter"), { once: true });
1281
+ setTimeout(() => row.classList.remove("enter"), 600);
1282
+ }
1283
+ els.timeline.insertBefore(row, prevNode ? prevNode.nextSibling : els.timeline.firstChild);
722
1284
  }
723
- dom.footerEvents.textContent = `eventos: ${state.events.length}` + (visible !== state.events.length ? ` (mostrando ${visible})` : '');
724
- }
725
-
726
- function pushVisibleEvent(evt) {
727
- state.events.push(evt);
728
- while (state.events.length > RING_DISPLAY_MAX) state.events.shift();
729
- while (dom.list.children.length > RING_DISPLAY_MAX - 1) dom.list.removeChild(dom.list.firstChild);
730
- const row = renderEventRow(evt);
731
- dom.list.appendChild(row);
732
- if (!shouldShow(evt)) row.style.display = 'none';
733
- applyFilter();
734
- if (state.autoscroll) {
735
- requestAnimationFrame(() => row.scrollIntoView({ block: 'end' }));
1285
+ const grouped = !!(visible[idx - 1] && visible[idx - 1].runId === evt.runId);
1286
+ row.dataset.grouped = grouped;
1287
+ prevNode = row;
1288
+ });
1289
+ // remove any leftover rows that no longer match
1290
+ existing.forEach((r) => r.remove());
1291
+ }
1292
+
1293
+ function rowHtml(evt, idx, prev) {
1294
+ const grouped = !!(prev && prev.runId === evt.runId);
1295
+ const ok = evt.payload?.ok;
1296
+ const time = clockTime(evt.ts);
1297
+ const rel = relTime(Date.now() - evt.ts);
1298
+ const badge = humanizeEventType(evt.type);
1299
+ const isNew = (Date.now() - evt.ts) < 1500;
1300
+ let msg = "";
1301
+ switch (evt.type) {
1302
+ case "run.start": {
1303
+ msg = `<strong>${escapeHtml(humanizeTool(evt.payload?.tool))}</strong> <span class="ident">${escapeHtml(evt.payload?.tool || "")}</span>${evt.payload?.target ? ` <span class="arrow">→</span> <span class="ident">${escapeHtml(evt.payload.target)}</span>` : ""}`;
1304
+ break;
736
1305
  }
737
- }
738
-
739
- function ingestEvent(evt) {
740
- // Always update the active-runs panel — that view is the live "what's
741
- // happening NOW" and shouldn't be affected by the pause toggle below.
742
- upsertActiveRun(evt);
743
-
744
- if (state.paused) {
745
- state.pausedBuffer.push(evt);
746
- dom.footerPaused.hidden = false;
747
- dom.footerPaused.textContent = `pausado: ${state.pausedBuffer.length} em fila`;
748
- return;
1306
+ case "run.end": {
1307
+ const dur = evt.payload?.duration_ms;
1308
+ msg = `<strong>${escapeHtml(humanizeTool(evt.payload?.tool))}</strong> ${ok ? "concluído" : "falhou"}${dur ? ` <span class="ident">${(dur/1000).toFixed(2)}s</span>` : ""}`;
1309
+ break;
749
1310
  }
750
- pushVisibleEvent(evt);
751
- if (evt.type === 'shutdown') {
752
- dom.banner.hidden = false;
1311
+ case "progress": {
1312
+ const pct = typeof evt.payload?.percent === "number" ? `${Math.round(evt.payload.percent)}%` : "";
1313
+ const lbl = evt.payload?.label || "";
1314
+ const ct = evt.payload?.current && evt.payload?.total ? ` <span class="ident">${evt.payload.current}/${evt.payload.total}</span>` : "";
1315
+ msg = `${pct ? `<strong>${pct}</strong> ` : ""}<span class="path">${escapeHtml(humanizePath(lbl))}</span>${ct}`;
1316
+ break;
753
1317
  }
754
- }
755
-
756
- // ------- Active runs ---------------------------------------------------
757
- // Maintain a Map<runId, ActiveRun> that tracks currently-executing tools.
758
- // Updated from run.start / progress / run.end / error events that share a
759
- // runId. Renders as cards above the event log so the user sees CURRENT
760
- // state at a glance instead of having to scan the chronological feed.
761
-
762
- const activeRuns = new Map(); // runId -> {tool, label, percent, current, total, startedAt, status, pendingRemove}
763
- const fadeTimers = new Map(); // runId -> setTimeout handle
764
-
765
- function upsertActiveRun(evt) {
766
- if (!evt.runId) return;
767
- const id = evt.runId;
768
- const p = evt.payload || {};
769
- let run = activeRuns.get(id);
770
-
771
- if (evt.type === 'run.start') {
772
- // Cancel any pending fade-out for a stale entry with the same id (rare).
773
- if (fadeTimers.has(id)) { clearTimeout(fadeTimers.get(id)); fadeTimers.delete(id); }
774
- activeRuns.set(id, {
775
- runId: id,
776
- tool: p.tool || p.server || 'run',
777
- label: 'iniciando…',
778
- percent: 0,
779
- current: null,
780
- total: null,
781
- startedAt: evt.ts,
782
- status: 'running',
783
- });
784
- } else if (evt.type === 'progress' && run && run.status === 'running') {
785
- if (typeof p.percent === 'number') run.percent = clampPercent(p.percent);
786
- if (typeof p.current === 'number') run.current = p.current;
787
- if (typeof p.total === 'number') run.total = p.total;
788
- if (typeof p.label === 'string' && p.label.length > 0) run.label = humanizeLabel(p.label);
789
- // If the wrapper sent current/total but no percent, derive it.
790
- if (typeof p.percent !== 'number' && typeof p.current === 'number' && typeof p.total === 'number' && p.total > 0) {
791
- run.percent = clampPercent(Math.round((p.current / p.total) * 100));
792
- }
793
- } else if (evt.type === 'tool_invocation' && run && run.status === 'running') {
794
- // tool_invocation events refine the title if it arrived after run.start.
795
- if (typeof p.tool === 'string') run.tool = p.tool;
796
- } else if (evt.type === 'run.end' && run) {
797
- run.status = (p && p.ok === false) ? 'error' : 'done';
798
- run.percent = run.status === 'done' ? 100 : run.percent;
799
- run.label = run.status === 'done' ? 'concluído com sucesso' : (p?.message || 'falhou');
800
- // Fade the card out a few seconds after completion so the user sees
801
- // the 100% and "done" before it disappears.
802
- const t = setTimeout(() => {
803
- activeRuns.delete(id);
804
- fadeTimers.delete(id);
805
- renderActiveRuns();
806
- }, 4000);
807
- fadeTimers.set(id, t);
808
- } else if (evt.type === 'error' && run && run.status === 'running') {
809
- run.status = 'error';
810
- run.label = p?.message || 'error';
811
- // Errors stay visible longer (8s) so the user has time to read.
812
- const t = setTimeout(() => {
813
- activeRuns.delete(id);
814
- fadeTimers.delete(id);
815
- renderActiveRuns();
816
- }, 8000);
817
- fadeTimers.set(id, t);
1318
+ case "milestone": {
1319
+ msg = `<strong>${escapeHtml(evt.payload?.name || "")}</strong>`;
1320
+ break;
818
1321
  }
819
- renderActiveRuns();
820
- }
821
-
822
- function clampPercent(n) {
823
- if (!Number.isFinite(n)) return 0;
824
- return Math.max(0, Math.min(100, n));
825
- }
826
-
827
- function fmtElapsed(startTs) {
828
- const sec = Math.max(0, Math.round((Date.now() - startTs) / 1000));
829
- if (sec < 60) return `${sec}s`;
830
- const m = Math.floor(sec / 60);
831
- const s = sec % 60;
832
- return `${m}m ${s.toString().padStart(2, '0')}s`;
833
- }
834
-
835
- function renderActiveRuns() {
836
- const runs = [...activeRuns.values()].sort((a, b) => a.startedAt - b.startedAt);
837
- if (runs.length === 0) {
838
- dom.activeRuns.hidden = true;
839
- dom.activeRunsList.replaceChildren();
840
- return;
1322
+ case "error": {
1323
+ msg = `<strong>${escapeHtml(evt.payload?.message || "erro")}</strong>${evt.payload?.code ? ` <span class="ident">${evt.payload.code}</span>` : ""}`;
1324
+ break;
841
1325
  }
842
- dom.activeRuns.hidden = false;
843
- dom.activeRunsCount.textContent = String(runs.length);
844
-
845
- // Use a stable key (runId) so we update existing cards in place rather than
846
- // recreating them (preserves the CSS transition on the progress bar width).
847
- const existing = new Map();
848
- for (const child of dom.activeRunsList.children) {
849
- existing.set(child.dataset.runid, child);
1326
+ case "tool_invocation": {
1327
+ msg = `<span class="ident">${escapeHtml(evt.payload?.tool || "")}</span>`;
1328
+ break;
850
1329
  }
851
-
852
- const frag = document.createDocumentFragment();
853
- for (const run of runs) {
854
- let card = existing.get(run.runId);
855
- if (!card) {
856
- card = el('div', { class: 'run-card', data: { runid: run.runId } });
857
- card.innerHTML = `
858
- <div class="run-title">
859
- <span class="run-name"></span>
860
- <span class="run-kind"></span>
861
- </div>
862
- <div class="run-percent"></div>
863
- <div class="run-bar-wrap"><div class="run-bar"></div></div>
864
- <div class="run-step"></div>
865
- <div class="run-meta"><span class="elapsed"></span><span class="runid"></span></div>
866
- `;
867
- }
868
- card.dataset.status = run.status;
869
- card.querySelector('.run-name').textContent = humanizeTool(run.tool);
870
- card.querySelector('.run-kind').textContent = humanizeStatus(run.status);
871
- card.querySelector('.run-percent').textContent = run.status === 'error' ? 'ERRO' : `${run.percent}%`;
872
- card.querySelector('.run-bar').style.width = `${run.percent}%`;
873
- card.querySelector('.run-step').textContent = run.label;
874
- const elapsed = card.querySelector('.elapsed');
875
- elapsed.textContent = `${fmtElapsed(run.startedAt)}`;
876
- const ridEl = card.querySelector('.runid');
877
- const progressTotalText = (run.current !== null && run.total !== null) ? ` · ${run.current}/${run.total}` : '';
878
- ridEl.textContent = `id ${run.runId.slice(0, 8)}${progressTotalText}`;
879
- frag.appendChild(card);
880
- }
881
- dom.activeRunsList.replaceChildren(frag);
882
- }
883
-
884
- // Tick the elapsed-time labels every second so they stay live.
885
- setInterval(() => {
886
- if (activeRuns.size === 0) return;
887
- for (const card of dom.activeRunsList.children) {
888
- const id = card.dataset.runid;
889
- const run = activeRuns.get(id);
890
- if (run) card.querySelector('.elapsed').textContent = `há ${fmtElapsed(run.startedAt)}`;
891
- }
892
- }, 1000);
893
-
894
- function flushPaused() {
895
- for (const evt of state.pausedBuffer) pushVisibleEvent(evt);
896
- state.pausedBuffer.length = 0;
897
- dom.footerPaused.hidden = true;
898
- }
899
-
900
- // ------- Filter UI ----------------------------------------------------
901
-
902
- for (const t of EVENT_TYPES) {
903
- const cb = el('input', { type: 'checkbox', checked: '' });
904
- cb.checked = true;
905
- cb.addEventListener('change', () => {
906
- if (cb.checked) state.typeFilter.add(t);
907
- else state.typeFilter.delete(t);
908
- applyFilter();
909
- });
910
- // Show humanized text but keep the data-type as the raw event name so
911
- // power users (and tests) can still target the underlying value.
912
- const lbl = el('label', { data: { type: t } }, [cb, document.createTextNode(humanizeEventType(t))]);
913
- dom.typeFilters.appendChild(lbl);
914
- }
915
-
916
- dom.search.addEventListener('input', () => {
917
- state.search = dom.search.value;
918
- applyFilter();
1330
+ default:
1331
+ msg = "";
1332
+ }
1333
+ return `
1334
+ <div class="tl-row" data-type="${evt.type}" data-ok="${ok}" data-grouped="${grouped}">
1335
+ <div class="tl-time" title="${time}">${rel}</div>
1336
+ <div class="tl-rail"><div class="tl-node"></div></div>
1337
+ <div class="tl-content">
1338
+ <span class="tl-badge">${badge}</span>
1339
+ <span class="tl-msg">${msg}</span>
1340
+ ${evt.runId ? `<span class="tl-runid">${evt.runId.slice(0,6)}</span>` : ""}
1341
+ </div>
1342
+ </div>
1343
+ `;
1344
+ }
1345
+
1346
+ function escapeHtml(s) {
1347
+ return String(s ?? "").replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
1348
+ }
1349
+
1350
+ /* ---------- elapsed ticker ---------- */
1351
+ setInterval(() => {
1352
+ for (const run of state.runs.values()) {
1353
+ if (run.ended) continue;
1354
+ const el = els.active.querySelector(`[data-runid="${run.runId}"] .rc-elapsed-val`);
1355
+ if (!el) continue;
1356
+ const elapsed = Date.now() - run.startTs;
1357
+ el.textContent = formatElapsed(elapsed);
1358
+ const wrap = el.closest(".em");
1359
+ if (wrap) wrap.classList.toggle("warn", elapsed > 30_000);
1360
+ }
1361
+ // refresh relative time on log rows (cheap: only update visible)
1362
+ els.timeline.querySelectorAll(".tl-time").forEach((node, i) => {
1363
+ const evt = state.events.filter(passesFilter)[i];
1364
+ if (evt) node.textContent = relTime(Date.now() - evt.ts);
919
1365
  });
920
-
921
- dom.pauseBtn.addEventListener('click', () => {
922
- state.paused = !state.paused;
923
- dom.pauseBtn.setAttribute('aria-pressed', String(state.paused));
924
- dom.pauseBtn.textContent = state.paused ? '▶ retomar' : '⏸ pausar';
925
- if (!state.paused) flushPaused();
1366
+ }, 1000);
1367
+
1368
+ /* ───────────────────── mock event generator ───────────────────── */
1369
+ let mockTimers = [];
1370
+ function clearMock() {
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
+ }));
926
1433
  });
927
-
928
- dom.autoscrollBtn.addEventListener('click', () => {
929
- state.autoscroll = !state.autoscroll;
930
- dom.autoscrollBtn.setAttribute('aria-pressed', String(state.autoscroll));
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
+
1461
+ function clearAll() {
1462
+ clearMock();
1463
+ state.events = [];
1464
+ state.runs.clear();
1465
+ state._seenKeys = new Set();
1466
+ els.active.innerHTML = "";
1467
+ render();
1468
+ els.lastSeen.textContent = "aguardando…";
1469
+ }
1470
+
1471
+ /* ───────────────────── tweaks wiring ───────────────────── */
1472
+ els.tweaksBtn.addEventListener("click", () => {
1473
+ const open = els.tweaks.classList.toggle("open");
1474
+ els.tweaksBtn.setAttribute("aria-pressed", open);
1475
+ });
1476
+ els.tweaksClose.addEventListener("click", () => {
1477
+ els.tweaks.classList.remove("open");
1478
+ els.tweaksBtn.setAttribute("aria-pressed", "false");
1479
+ });
1480
+
1481
+ document.querySelectorAll("#tw-accent button").forEach((b) => {
1482
+ b.addEventListener("click", () => {
1483
+ document.querySelectorAll("#tw-accent button").forEach(x => x.setAttribute("aria-pressed","false"));
1484
+ b.setAttribute("aria-pressed","true");
1485
+ document.documentElement.style.setProperty("--accent-h", b.dataset.h);
931
1486
  });
932
-
933
- dom.clearBtn.addEventListener('click', () => {
934
- state.events.length = 0;
935
- while (dom.list.firstChild) dom.list.removeChild(dom.list.firstChild);
936
- applyFilter();
1487
+ });
1488
+ document.querySelector('#tw-accent button[data-h="130"]').setAttribute("aria-pressed","true");
1489
+
1490
+ function wireSeg(rootSel, attr, fn) {
1491
+ document.querySelectorAll(`${rootSel} button`).forEach((b) => {
1492
+ b.addEventListener("click", () => {
1493
+ document.querySelectorAll(`${rootSel} button`).forEach(x => x.setAttribute("aria-pressed","false"));
1494
+ b.setAttribute("aria-pressed","true");
1495
+ fn(b.dataset.v);
1496
+ });
1497
+ });
1498
+ }
1499
+ wireSeg("#tw-density", "v", (v) => document.documentElement.dataset.density = v);
1500
+ wireSeg("#tw-motion", "v", (v) => document.documentElement.dataset.motion = v);
1501
+ wireSeg("#tw-scenario","v", (v) => { clearAll(); runScenario(v); });
1502
+
1503
+ document.documentElement.dataset.density = "normal";
1504
+ document.documentElement.dataset.motion = "medium";
1505
+
1506
+ document.getElementById("tw-replay").addEventListener("click", () => { clearAll(); runScenario(currentScenario); });
1507
+ document.getElementById("tw-clear").addEventListener("click", clearAll);
1508
+
1509
+ /* ───────────────────── toolbar wiring ───────────────────── */
1510
+ els.q.addEventListener("input", (e) => { state.filterText = e.target.value.trim(); render(); });
1511
+ document.addEventListener("keydown", (e) => {
1512
+ if (e.key === "/" && document.activeElement !== els.q) {
1513
+ e.preventDefault();
1514
+ els.q.focus();
1515
+ }
1516
+ if (e.key === "Escape" && document.activeElement === els.q) {
1517
+ els.q.value = ""; state.filterText = ""; els.q.blur(); render();
1518
+ }
1519
+ });
1520
+
1521
+ els.filterBtn.addEventListener("click", (e) => {
1522
+ e.stopPropagation();
1523
+ const open = els.filterPop.classList.toggle("open");
1524
+ els.filterBtn.setAttribute("aria-pressed", open);
1525
+ });
1526
+ document.addEventListener("click", (e) => {
1527
+ if (!els.filterPop.contains(e.target) && e.target !== els.filterBtn) {
1528
+ els.filterPop.classList.remove("open");
1529
+ els.filterBtn.setAttribute("aria-pressed", "false");
1530
+ }
1531
+ });
1532
+ els.filterPop.querySelectorAll(".fp-row").forEach((row) => {
1533
+ row.addEventListener("click", () => {
1534
+ const f = row.dataset.filter;
1535
+ const on = row.dataset.on === "true";
1536
+ row.dataset.on = on ? "false" : "true";
1537
+ if (on) state.filters.delete(f); else state.filters.add(f);
1538
+ render();
937
1539
  });
1540
+ });
1541
+
1542
+ els.pauseBtn.addEventListener("click", () => {
1543
+ state.paused = !state.paused;
1544
+ els.pauseBtn.setAttribute("aria-pressed", state.paused);
1545
+ els.pauseIcon.innerHTML = state.paused
1546
+ ? '<polygon points="6,5 19,12 6,19" fill="currentColor"/>'
1547
+ : '<rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/>';
1548
+ els.conn.dataset.state = state.paused ? "off" : "on";
1549
+ els.connLabel.textContent = state.paused ? "pausado" : "conectado";
1550
+ els.srcLabel.textContent = state.paused ? "pausado" : "ao vivo";
1551
+ });
1552
+
1553
+ /* ───────────────────── real source: SSE + /state hydrate ───────────────────── */
1554
+ /* The prototype above keeps mock scenarios available via the tweaks panel. In
1555
+ production we connect to /events for the live feed and to /state to backfill
1556
+ the ring buffer on first paint and after reconnect. */
1557
+
1558
+ let evtSource = null;
1559
+ let lastConnectedAt = 0;
1560
+
1561
+ function applyConnState(s) {
1562
+ // s ∈ { 'connecting', 'open', 'closed', 'paused', 'shutdown' }
1563
+ if (s === "open") {
1564
+ els.conn.dataset.state = "on";
1565
+ els.connLabel.textContent = "conectado";
1566
+ if (els.srcLabel) els.srcLabel.textContent = "ao vivo";
1567
+ } else if (s === "connecting") {
1568
+ els.conn.dataset.state = "off";
1569
+ els.connLabel.textContent = "conectando";
1570
+ if (els.srcLabel) els.srcLabel.textContent = "conectando";
1571
+ } else if (s === "closed") {
1572
+ els.conn.dataset.state = "off";
1573
+ els.connLabel.textContent = "desconectado";
1574
+ if (els.srcLabel) els.srcLabel.textContent = "offline";
1575
+ } else if (s === "shutdown") {
1576
+ els.conn.dataset.state = "off";
1577
+ els.connLabel.textContent = "encerrado";
1578
+ if (els.srcLabel) els.srcLabel.textContent = "encerrado";
1579
+ } else if (s === "paused") {
1580
+ els.conn.dataset.state = "off";
1581
+ els.connLabel.textContent = "pausado";
1582
+ if (els.srcLabel) els.srcLabel.textContent = "pausado";
1583
+ }
1584
+ }
1585
+
1586
+ async function hydrateFromState() {
1587
+ try {
1588
+ const res = await fetch("/state", { credentials: "omit" });
1589
+ if (!res.ok) return;
1590
+ const j = await res.json();
1591
+ if (j.port && document.querySelector(".brand-sub")) {
1592
+ document.querySelector(".brand-sub").textContent = "127.0.0.1:" + j.port;
1593
+ }
1594
+ if (j.version && document.getElementById("brand-meta")) {
1595
+ document.getElementById("brand-meta").textContent = "sidecar · v" + j.version;
1596
+ }
1597
+ if (Array.isArray(j.events)) {
1598
+ // Replay through ingest. The dedup key (ts|type|runId) prevents
1599
+ // double-counting if SSE later delivers the same event.
1600
+ for (const evt of j.events) ingest(evt);
1601
+ }
1602
+ } catch (_) { /* fine — SSE may still work */ }
1603
+ }
938
1604
 
939
- // ------- Connection lifecycle ----------------------------------------
1605
+ function connectRealSource() {
1606
+ applyConnState("connecting");
1607
+ if (evtSource) try { evtSource.close(); } catch (_) {}
940
1608
 
941
- let evtSource = null;
942
- let closedTimer = null;
943
- let lastConnectedAt = 0;
1609
+ evtSource = new EventSource("/events");
944
1610
 
945
- function setConnState(s) {
946
- dom.conn.dataset.state = s;
947
- dom.connText.textContent = CONN_LABEL[s] || s;
948
- }
1611
+ evtSource.addEventListener("open", () => {
1612
+ lastConnectedAt = Date.now();
1613
+ applyConnState("open");
1614
+ });
949
1615
 
950
- function scheduleClosedBanner() {
951
- if (closedTimer) clearTimeout(closedTimer);
952
- closedTimer = setTimeout(() => {
953
- // Only show shutdown banner if still closed after 5s
954
- if (dom.conn.dataset.state === 'CLOSED') {
955
- dom.banner.hidden = false;
956
- }
957
- }, 5000);
958
- }
1616
+ evtSource.addEventListener("error", () => {
1617
+ // EventSource auto-retries; reflect "closed" until next open fires.
1618
+ applyConnState("closed");
1619
+ });
959
1620
 
960
- async function hydrateFromState() {
1621
+ // Listen for each typed event the server emits.
1622
+ const handler = (msg) => {
961
1623
  try {
962
- const res = await fetch('/state', { credentials: 'omit' });
963
- if (!res.ok) return;
964
- const j = await res.json();
965
- if (j.port) dom.metaPort.textContent = `porta ${j.port}`;
966
- if (Array.isArray(j.events)) {
967
- for (const evt of j.events) ingestEvent(evt);
1624
+ const data = JSON.parse(msg.data);
1625
+ if (data && data.type === "shutdown") {
1626
+ applyConnState("shutdown");
1627
+ showShutdownBanner();
968
1628
  }
969
- } catch (_) {
970
- // Ignore SSE may still work
971
- }
972
- }
973
-
974
- function connect() {
975
- setConnState('CONNECTING');
976
- if (evtSource) try { evtSource.close(); } catch (_) { /* noop */ }
977
- evtSource = new EventSource('/events');
978
- evtSource.onopen = () => {
979
- setConnState('OPEN');
980
- lastConnectedAt = Date.now();
981
- if (closedTimer) { clearTimeout(closedTimer); closedTimer = null; }
982
- // Don't hide banner if we already received a 'shutdown' event
983
- };
984
- evtSource.onerror = () => {
985
- setConnState('CLOSED');
986
- scheduleClosedBanner();
987
- // EventSource will retry automatically (we send retry: 3000 from server)
988
- };
989
- // Listen for each known event type so 'event:' lines get routed to the same handler
990
- const handler = (msg) => {
991
- try {
992
- const data = JSON.parse(msg.data);
993
- ingestEvent(data);
994
- } catch (_) { /* swallow malformed */ }
995
- };
996
- for (const t of EVENT_TYPES) evtSource.addEventListener(t, handler);
997
- evtSource.onmessage = handler; // fallback for events without type field
998
- }
999
-
1000
- // ------- Background-tab recovery -------------------------------------
1001
- // Chrome aggressively throttles background tabs; the native EventSource
1002
- // retry timer can get suspended, leaving the connection stuck in CLOSED
1003
- // when the user comes back. The Page Visibility API lets us force a fresh
1004
- // connect() when the tab becomes visible again and we know we're CLOSED.
1005
- document.addEventListener('visibilitychange', () => {
1006
- if (document.visibilityState !== 'visible') return;
1007
- if (dom.conn.dataset.state === 'CLOSED') {
1008
- // Re-hydrate from /state in case events arrived while we were dropped,
1009
- // then reopen the SSE stream.
1010
- hydrateFromState().then(connect);
1011
- }
1012
- });
1013
-
1014
- // ------- Boot ---------------------------------------------------------
1015
-
1016
- hydrateFromState().then(() => {
1017
- connect();
1018
- applyFilter();
1629
+ ingest(data);
1630
+ } catch (_) { /* swallow malformed */ }
1631
+ };
1632
+ ["run.start", "run.end", "tool_invocation", "progress", "milestone", "error", "shutdown"].forEach((t) =>
1633
+ evtSource.addEventListener(t, handler)
1634
+ );
1635
+ evtSource.onmessage = handler; // fallback for unnamed
1636
+ }
1637
+
1638
+ /* Background-tab recovery Chrome throttles timers in inactive tabs and the
1639
+ native EventSource retry can stall. When the tab becomes visible again and
1640
+ we know we're closed, force a fresh hydrate + reconnect. */
1641
+ document.addEventListener("visibilitychange", () => {
1642
+ if (document.visibilityState !== "visible") return;
1643
+ if (els.conn.dataset.state === "off") {
1644
+ hydrateFromState().then(connectRealSource);
1645
+ }
1646
+ });
1647
+
1648
+ /* ───────────────────── shutdown banner ───────────────────── */
1649
+ function showShutdownBanner() {
1650
+ if (document.getElementById("shutdown-banner")) return;
1651
+ const banner = document.createElement("div");
1652
+ banner.id = "shutdown-banner";
1653
+ banner.innerHTML = `
1654
+ <strong>Sidecar encerrou.</strong>
1655
+ Reabra com <span style="font-family:var(--mono);color:var(--text-2)">kit ui start</span> e atualize esta página.
1656
+ `;
1657
+ Object.assign(banner.style, {
1658
+ margin: "0 0 16px",
1659
+ padding: "12px 14px",
1660
+ border: "1px solid var(--err-soft)",
1661
+ borderRadius: "var(--radius)",
1662
+ background: "color-mix(in srgb, var(--err) 10%, var(--surface-1))",
1663
+ color: "var(--err)",
1664
+ fontSize: "12px",
1665
+ display: "flex",
1666
+ alignItems: "center",
1667
+ gap: "8px",
1019
1668
  });
1669
+ const main = document.querySelector("main");
1670
+ if (main) main.insertBefore(banner, main.firstChild);
1671
+ }
1672
+
1673
+ /* ───────────────────── boot ───────────────────── */
1674
+ /* In production we DON'T auto-run the mock — the real /events stream is the
1675
+ source of truth. The mock scenarios are still wired and accessible through
1676
+ the tweaks panel for demo/dev. */
1677
+
1678
+ (async () => {
1679
+ await hydrateFromState();
1680
+ connectRealSource();
1681
+ })();
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
+ }
1020
1690
  </script>
1021
1691
  </body>
1022
1692
  </html>