@luanpdd/kit-mcp 1.2.0 → 1.3.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,609 +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
- }
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);
45
68
  }
69
+ }
46
70
 
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
- }
71
+ /* density tweak */
72
+ :root[data-density="compact"] { --pad: 10px; --pad-tight: 6px; }
73
+ :root[data-density="comfy"] { --pad: 20px; --pad-tight: 14px; }
58
74
 
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
- }
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); }
161
93
 
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
- }
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); }
181
99
 
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
- }
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
+ }
197
109
 
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
- }
110
+ @media (max-width: 520px) {
111
+ .app { padding: 12px 14px 0; }
112
+ }
225
113
 
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
- }
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); }
277
172
 
278
- .banner {
279
- margin: 12px 0 0;
280
- padding: 12px 16px;
281
- border-radius: 8px;
282
- background: color-mix(in srgb, var(--err) 8%, var(--bg-elev));
283
- border: 1px solid var(--err);
284
- color: var(--err);
285
- font-size: 13px;
286
- }
287
- .banner[hidden] { display: none; }
288
- .banner code {
289
- font-family: var(--mono);
290
- background: var(--bg-row);
291
- padding: 1px 5px;
292
- border-radius: 4px;
293
- color: var(--fg-muted);
294
- }
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; } }
295
293
 
296
- .footer {
297
- padding: 8px 16px;
298
- border-top: 1px solid var(--border);
299
- background: var(--bg-elev);
300
- color: var(--fg-subtle);
301
- font-family: var(--mono);
302
- font-size: 11px;
303
- display: flex;
304
- gap: 16px;
305
- flex-wrap: wrap;
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;
306
813
  }
814
+ }
307
815
  </style>
308
816
  </head>
309
817
  <body>
310
- <header>
311
- <h1>kit-mcp sidecar</h1>
312
- <span class="meta" id="meta-port">port —</span>
313
- <span class="grow"></span>
314
- <span class="conn-status" id="conn" data-state="CONNECTING"><span class="dot"></span><span id="conn-text">CONNECTING</span></span>
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>
315
840
  </header>
316
841
 
842
+ <!-- TOOLBAR -->
317
843
  <div class="toolbar">
318
- <input type="search" id="search" placeholder="filter by label or payload…" autocomplete="off">
319
- <fieldset class="filters" id="type-filters" aria-label="Event types">
320
- <!-- populated from EVENT_TYPES at runtime -->
321
- </fieldset>
322
- <button id="pause-btn" aria-pressed="false">⏸ pause</button>
323
- <button id="autoscroll-btn" aria-pressed="true">↧ autoscroll</button>
324
- <button id="clear-btn" title="Clear visible events (ring buffer untouched)">clear view</button>
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>
325
877
  </div>
326
878
 
879
+ <!-- ACTIVE + LOG -->
327
880
  <main>
328
- <div class="banner" id="shutdown-banner" hidden>
329
- <strong>Sidecar encerrou.</strong> Recarregue depois de <code>kit ui start</code>.
330
- </div>
331
- <ul class="ev-list" id="events" hidden></ul>
332
- <div class="empty" id="empty">
333
- <strong>Aguardando primeiro evento…</strong>
334
- Rode <code>kit sync install</code>, <code>kit reverse-sync apply</code>, ou
335
- invoke uma tool MCP com <code>autoSpawn: true</code> em outra janela.
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>
336
886
  </div>
887
+
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>
902
+ </div>
903
+ </section>
337
904
  </main>
338
905
 
339
- <div class="footer">
340
- <span id="footer-events">events: 0</span>
341
- <span id="footer-paused" hidden>paused: 0 buffered</span>
342
- <span id="footer-source">source: live</span>
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>
937
+
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>
946
+
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>
955
+
956
+ <div class="tw-group">
957
+ <span class="tw-label">Cenário (mock)</span>
958
+ <div class="seg" id="tw-scenario">
959
+ <button data-v="sync" aria-pressed="true">Sync</button>
960
+ <button data-v="multi">Multi</button>
961
+ <button data-v="error">Erro</button>
962
+ <button data-v="idle">Idle</button>
963
+ </div>
964
+ </div>
965
+
966
+ <div class="tw-actions">
967
+ <button id="tw-replay">▸ replay</button>
968
+ <button id="tw-clear">limpar</button>
969
+ </div>
343
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>
344
994
 
345
995
  <script>
346
- // src/ui/static/index.html — vanilla DOM client for the kit-mcp sidecar.
347
- // Connects to /events (SSE) and hydrates from /state on load.
348
- // No build step. No deps.
349
-
350
- 'use strict';
351
-
352
- const EVENT_TYPES = ['run.start', 'run.end', 'tool_invocation', 'progress', 'milestone', 'error', 'shutdown'];
353
- const RING_DISPLAY_MAX = 500;
354
-
355
- const $ = (id) => document.getElementById(id);
356
- const dom = {
357
- conn: $('conn'),
358
- connText: $('conn-text'),
359
- metaPort: $('meta-port'),
360
- list: $('events'),
361
- empty: $('empty'),
362
- banner: $('shutdown-banner'),
363
- pauseBtn: $('pause-btn'),
364
- autoscrollBtn: $('autoscroll-btn'),
365
- clearBtn: $('clear-btn'),
366
- search: $('search'),
367
- typeFilters: $('type-filters'),
368
- footerEvents: $('footer-events'),
369
- footerPaused: $('footer-paused'),
370
- footerSource: $('footer-source'),
371
- };
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
+ ────────────────────────────────────────────────────────── */
372
1001
 
373
- const state = {
374
- events: [], // currently rendered events (buffered while paused)
375
- pausedBuffer: [], // events captured while paused
376
- paused: false,
377
- autoscroll: true,
378
- typeFilter: new Set(EVENT_TYPES), // all enabled
379
- search: '',
380
- closedAt: null,
381
- };
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
+ }
382
1039
 
383
- // ------- DOM helpers --------------------------------------------------
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
+ }
384
1056
 
385
- function el(tag, props = {}, kids = []) {
386
- const e = document.createElement(tag);
387
- for (const [k, v] of Object.entries(props)) {
388
- if (k === 'class') e.className = v;
389
- else if (k === 'data') for (const [dk, dv] of Object.entries(v)) e.dataset[dk] = dv;
390
- else if (k.startsWith('on')) e.addEventListener(k.slice(2).toLowerCase(), v);
391
- else if (k === 'text') e.textContent = v;
392
- else if (v !== undefined && v !== null) e.setAttribute(k, v);
393
- }
394
- for (const k of kids) if (k) e.appendChild(typeof k === 'string' ? document.createTextNode(k) : k);
395
- return e;
396
- }
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
+ };
397
1066
 
398
- function fmtTime(ts) {
399
- const d = new Date(ts);
400
- const hh = String(d.getHours()).padStart(2, '0');
401
- const mm = String(d.getMinutes()).padStart(2, '0');
402
- const ss = String(d.getSeconds()).padStart(2, '0');
403
- return `${hh}:${mm}:${ss}`;
404
- }
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
+ };
405
1089
 
406
- function eventLabel(evt) {
407
- // Best-effort short label from payload; fallback to type.
408
- const p = evt.payload;
409
- if (!p || typeof p !== 'object') return evt.type;
410
- if (typeof p.label === 'string') return p.label;
411
- if (typeof p.tool === 'string') return `tool: ${p.tool}`;
412
- if (typeof p.percent === 'number') return `${p.percent}%${p.kind ? ' · ' + p.kind : ''}`;
413
- if (typeof p.message === 'string') return p.message;
414
- if (typeof p.reason === 'string') return p.reason;
415
- return evt.type;
416
- }
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;
417
1095
 
418
- function eventMeta(evt) {
419
- const p = evt.payload;
420
- if (evt.type === 'progress' && p && typeof p.total === 'number' && typeof p.current === 'number') {
421
- return `${p.current}/${p.total}`;
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);
1102
+ }
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
422
1118
  }
423
- if (evt.runId) return evt.runId.slice(0, 6);
424
- return '';
425
1119
  }
426
1120
 
427
- function renderEventRow(evt) {
428
- const time = el('div', { class: 'ev-time', text: fmtTime(evt.ts) });
429
- const badge = el('span', { class: 'ev-badge', data: { type: evt.type }, text: evt.type });
430
- const summary = el('summary', {}, [
431
- el('span', { class: 'label', text: eventLabel(evt) }),
432
- el('span', { class: 'meta', text: eventMeta(evt) }),
433
- ]);
434
- const pre = el('pre', { text: JSON.stringify(evt.payload ?? null, null, 2) });
435
- const details = el('details', {}, [summary, pre]);
436
- const body = el('div', { class: 'ev-body' }, [details]);
437
- return el('li', { class: 'ev-row', data: { type: evt.type, label: eventLabel(evt).toLowerCase() } }, [time, badge, body]);
438
- }
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
+ }
439
1133
 
440
- function shouldShow(evt) {
441
- if (!state.typeFilter.has(evt.type)) return false;
442
- if (state.search) {
443
- const haystack = (evt.type + ' ' + eventLabel(evt) + ' ' + JSON.stringify(evt.payload ?? '')).toLowerCase();
444
- if (!haystack.includes(state.search.toLowerCase())) return false;
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
+ });
1151
+
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);
445
1163
  }
446
- return true;
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>
1211
+
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>
1216
+
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>
1222
+
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;
447
1247
  }
1248
+ els.empty.style.display = "none";
1249
+ els.logHead.hidden = false;
448
1250
 
449
- function applyFilter() {
450
- const rows = dom.list.children;
451
- let visible = 0;
452
- for (let i = 0; i < rows.length; i += 1) {
453
- const row = rows[i];
454
- const evt = state.events[i];
455
- const show = evt && shouldShow(evt);
456
- row.style.display = show ? '' : 'none';
457
- if (show) visible += 1;
458
- }
459
- dom.empty.hidden = visible > 0 || state.events.length > 0;
460
- dom.list.hidden = visible === 0 && state.events.length > 0 ? false : visible === 0;
461
- if (state.events.length === 0) {
462
- dom.empty.hidden = false;
463
- dom.list.hidden = true;
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
+ }
464
1270
  } else {
465
- dom.empty.hidden = true;
466
- 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);
467
1284
  }
468
- dom.footerEvents.textContent = `events: ${state.events.length}` + (visible !== state.events.length ? ` (showing ${visible})` : '');
469
- }
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
+ }
470
1292
 
471
- function pushVisibleEvent(evt) {
472
- state.events.push(evt);
473
- while (state.events.length > RING_DISPLAY_MAX) state.events.shift();
474
- while (dom.list.children.length > RING_DISPLAY_MAX - 1) dom.list.removeChild(dom.list.firstChild);
475
- const row = renderEventRow(evt);
476
- dom.list.appendChild(row);
477
- if (!shouldShow(evt)) row.style.display = 'none';
478
- applyFilter();
479
- if (state.autoscroll) {
480
- requestAnimationFrame(() => row.scrollIntoView({ block: 'end' }));
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;
481
1305
  }
482
- }
483
-
484
- function ingestEvent(evt) {
485
- if (state.paused) {
486
- state.pausedBuffer.push(evt);
487
- dom.footerPaused.hidden = false;
488
- dom.footerPaused.textContent = `paused: ${state.pausedBuffer.length} buffered`;
489
- 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;
1310
+ }
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;
1317
+ }
1318
+ case "milestone": {
1319
+ msg = `<strong>${escapeHtml(evt.payload?.name || "")}</strong>`;
1320
+ break;
490
1321
  }
491
- pushVisibleEvent(evt);
492
- if (evt.type === 'shutdown') {
493
- dom.banner.hidden = false;
1322
+ case "error": {
1323
+ msg = `<strong>${escapeHtml(evt.payload?.message || "erro")}</strong>${evt.payload?.code ? ` <span class="ident">${evt.payload.code}</span>` : ""}`;
1324
+ break;
494
1325
  }
1326
+ case "tool_invocation": {
1327
+ msg = `<span class="ident">${escapeHtml(evt.payload?.tool || "")}</span>`;
1328
+ break;
1329
+ }
1330
+ default:
1331
+ msg = "";
495
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
+ }
496
1349
 
497
- function flushPaused() {
498
- for (const evt of state.pausedBuffer) pushVisibleEvent(evt);
499
- state.pausedBuffer.length = 0;
500
- dom.footerPaused.hidden = true;
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);
501
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);
1365
+ });
1366
+ }, 1000);
502
1367
 
503
- // ------- Filter UI ----------------------------------------------------
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)); }
504
1375
 
505
- for (const t of EVENT_TYPES) {
506
- const cb = el('input', { type: 'checkbox', checked: '' });
507
- cb.checked = true;
508
- cb.addEventListener('change', () => {
509
- if (cb.checked) state.typeFilter.add(t);
510
- else state.typeFilter.delete(t);
511
- applyFilter();
512
- });
513
- const lbl = el('label', {}, [cb, document.createTextNode(t)]);
514
- dom.typeFilters.appendChild(lbl);
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
+ }));
515
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
+ }
516
1421
 
517
- dom.search.addEventListener('input', () => {
518
- state.search = dom.search.value;
519
- applyFilter();
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
+ }));
520
1433
  });
1434
+ later(800 + stages.length * 1100 + 300, () => ingest({
1435
+ type: "run.end", ts: Date.now(), runId, payload: { tool: "gates.run", ok: true, duration_ms: stages.length * 1100 }
1436
+ }));
1437
+ }
521
1438
 
522
- dom.pauseBtn.addEventListener('click', () => {
523
- state.paused = !state.paused;
524
- dom.pauseBtn.setAttribute('aria-pressed', String(state.paused));
525
- dom.pauseBtn.textContent = state.paused ? '▶ resume' : '⏸ pause';
526
- if (!state.paused) flushPaused();
527
- });
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
+ }
528
1447
 
529
- dom.autoscrollBtn.addEventListener('click', () => {
530
- state.autoscroll = !state.autoscroll;
531
- dom.autoscrollBtn.setAttribute('aria-pressed', String(state.autoscroll));
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);
532
1486
  });
1487
+ });
1488
+ document.querySelector('#tw-accent button[data-h="130"]').setAttribute("aria-pressed","true");
533
1489
 
534
- dom.clearBtn.addEventListener('click', () => {
535
- state.events.length = 0;
536
- while (dom.list.firstChild) dom.list.removeChild(dom.list.firstChild);
537
- applyFilter();
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
+ });
538
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); });
539
1502
 
540
- // ------- Connection lifecycle ----------------------------------------
1503
+ document.documentElement.dataset.density = "normal";
1504
+ document.documentElement.dataset.motion = "medium";
541
1505
 
542
- let evtSource = null;
543
- let closedTimer = null;
544
- let lastConnectedAt = 0;
1506
+ document.getElementById("tw-replay").addEventListener("click", () => { clearAll(); runScenario(currentScenario); });
1507
+ document.getElementById("tw-clear").addEventListener("click", clearAll);
545
1508
 
546
- function setConnState(s) {
547
- dom.conn.dataset.state = s;
548
- dom.connText.textContent = s;
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();
549
1515
  }
1516
+ if (e.key === "Escape" && document.activeElement === els.q) {
1517
+ els.q.value = ""; state.filterText = ""; els.q.blur(); render();
1518
+ }
1519
+ });
550
1520
 
551
- function scheduleClosedBanner() {
552
- if (closedTimer) clearTimeout(closedTimer);
553
- closedTimer = setTimeout(() => {
554
- // Only show shutdown banner if still closed after 5s
555
- if (dom.conn.dataset.state === 'CLOSED') {
556
- dom.banner.hidden = false;
557
- }
558
- }, 5000);
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");
559
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();
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. */
560
1557
 
561
- async function hydrateFromState() {
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
+ }
1604
+
1605
+ function connectRealSource() {
1606
+ applyConnState("connecting");
1607
+ if (evtSource) try { evtSource.close(); } catch (_) {}
1608
+
1609
+ evtSource = new EventSource("/events");
1610
+
1611
+ evtSource.addEventListener("open", () => {
1612
+ lastConnectedAt = Date.now();
1613
+ applyConnState("open");
1614
+ });
1615
+
1616
+ evtSource.addEventListener("error", () => {
1617
+ // EventSource auto-retries; reflect "closed" until next open fires.
1618
+ applyConnState("closed");
1619
+ });
1620
+
1621
+ // Listen for each typed event the server emits.
1622
+ const handler = (msg) => {
562
1623
  try {
563
- const res = await fetch('/state', { credentials: 'omit' });
564
- if (!res.ok) return;
565
- const j = await res.json();
566
- if (j.port) dom.metaPort.textContent = `port ${j.port}`;
567
- if (Array.isArray(j.events)) {
568
- 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();
569
1628
  }
570
- } catch (_) {
571
- // Ignore SSE may still work
572
- }
573
- }
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
+ }
574
1637
 
575
- function connect() {
576
- setConnState('CONNECTING');
577
- if (evtSource) try { evtSource.close(); } catch (_) { /* noop */ }
578
- evtSource = new EventSource('/events');
579
- evtSource.onopen = () => {
580
- setConnState('OPEN');
581
- lastConnectedAt = Date.now();
582
- if (closedTimer) { clearTimeout(closedTimer); closedTimer = null; }
583
- // Don't hide banner if we already received a 'shutdown' event
584
- };
585
- evtSource.onerror = () => {
586
- setConnState('CLOSED');
587
- scheduleClosedBanner();
588
- // EventSource will retry automatically (we send retry: 3000 from server)
589
- };
590
- // Listen for each known event type so 'event:' lines get routed to the same handler
591
- const handler = (msg) => {
592
- try {
593
- const data = JSON.parse(msg.data);
594
- ingestEvent(data);
595
- } catch (_) { /* swallow malformed */ }
596
- };
597
- for (const t of EVENT_TYPES) evtSource.addEventListener(t, handler);
598
- evtSource.onmessage = handler; // fallback for events without type field
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);
599
1645
  }
1646
+ });
600
1647
 
601
- // ------- Boot ---------------------------------------------------------
602
-
603
- hydrateFromState().then(() => {
604
- connect();
605
- applyFilter();
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",
606
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
+ }
607
1690
  </script>
608
1691
  </body>
609
1692
  </html>