@zerotal/devtools 1.6.3 → 1.7.2

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +329 -0
  2. package/api-surface.md +298 -0
  3. package/package.json +5 -4
  4. package/src/DevtoolsInjectionMiddleware.ts +41 -4
  5. package/src/RequestTrace.ts +106 -1
  6. package/src/TraceStore.ts +12 -0
  7. package/src/activity.ts +116 -0
  8. package/src/callsite.ts +146 -0
  9. package/src/client/filter.ts +108 -0
  10. package/src/client/index.ts +127 -0
  11. package/src/client/metrics.ts +98 -0
  12. package/src/client/registry.ts +87 -0
  13. package/src/client/state.ts +350 -0
  14. package/src/client/tabs/all.ts +323 -0
  15. package/src/client/tabs/app.ts +293 -0
  16. package/src/client/tabs/cache.ts +50 -0
  17. package/src/client/tabs/channel.ts +264 -0
  18. package/src/client/tabs/exceptions.ts +69 -0
  19. package/src/client/tabs/jobs.ts +51 -0
  20. package/src/client/tabs/live.ts +66 -0
  21. package/src/client/tabs/logs.ts +45 -0
  22. package/src/client/tabs/mail.ts +60 -0
  23. package/src/client/tabs/queries.ts +125 -0
  24. package/src/client/tabs/request.ts +75 -0
  25. package/src/client/tabs/sections.ts +115 -0
  26. package/src/client/tabs/timeline.ts +133 -0
  27. package/src/client/tabs/types.ts +68 -0
  28. package/src/client/transport.ts +81 -0
  29. package/src/client/tree.ts +138 -0
  30. package/src/client/ui/format.ts +137 -0
  31. package/src/client/ui/render.ts +87 -0
  32. package/src/client/ui/shell.ts +560 -0
  33. package/src/client/ui/theme.ts +445 -0
  34. package/src/client-auto.ts +1 -1
  35. package/src/config.ts +77 -2
  36. package/src/dashboard-auto.ts +1 -1
  37. package/src/editor.ts +107 -0
  38. package/src/enabled.ts +59 -0
  39. package/src/index.ts +19 -3
  40. package/src/map.ts +213 -0
  41. package/src/provider/DevtoolsProvider.ts +32 -7
  42. package/src/redaction.ts +161 -20
  43. package/src/tracing.ts +261 -29
  44. package/src/client.ts +0 -1048
  45. package/src/panel-app.js +0 -519
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Every style the panel has, as one string injected into its shadow root.
3
+ *
4
+ * Two palettes, one set of rules. The tokens are declared twice — once on `#wrap`
5
+ * and once on `#wrap.light` — and a class decides which applies, rather than a
6
+ * media query deciding it in CSS. That keeps the "follow the system" case and the
7
+ * explicit override from fighting each other over specificity: the panel resolves
8
+ * the three-way choice (auto / light / dark) in one place in JavaScript and the
9
+ * stylesheet only ever sees the answer.
10
+ */
11
+
12
+ /** The one place a colour is named. Every rule below reads these through `var()`. */
13
+ const TOKENS = `
14
+ #wrap {
15
+ --bg: #1a1b26; --surf: #24283b; --card: #2f3452; --bdr: #3b4261;
16
+ /* --muted carries labels, hints and every inactive tab. At the palette's own
17
+ #565f89 that is 2.35:1 on --surf — below AA for text of any size, and this
18
+ is 10–11px text. Lifted until it clears 4.5:1 while staying recessive. */
19
+ --text: #c0caf5; --muted: #8790bd; --purple: #7aa2f7;
20
+ --green: #9ece6a; --yellow: #e0af68; --red: #f7768e;
21
+ --cyan: #7dcfff; --orange: #ff9e64;
22
+ --childbg: rgba(0,0,0,.15);
23
+ }
24
+ #wrap.light {
25
+ --bg: #f4f4f8; --surf: #e8e9f0; --card: #dcdee8; --bdr: #c0c4d4;
26
+ --text: #343b58; --muted: #565c7d; --purple: #34548a;
27
+ --green: #33635c; --yellow: #8f5e15; --red: #c64343;
28
+ --cyan: #0f4b6e; --orange: #965027;
29
+ --childbg: rgba(0,0,0,.05);
30
+ }`;
31
+
32
+ export const CSS = `<style>
33
+ :host { all: initial; }
34
+ * { box-sizing: border-box; margin: 0; padding: 0; }
35
+ ${TOKENS}
36
+ #wrap {
37
+ font: 12px/1.5 'JetBrains Mono','Fira Code','SF Mono',ui-monospace,monospace;
38
+ color: var(--text);
39
+ }
40
+ #wrap:focus { outline: none; }
41
+ /* The container itself takes focus on open and should not draw a ring for it.
42
+ Anything a person actually tabs to must — the panel used to suppress focus
43
+ everywhere, which left keyboard navigation invisible. */
44
+ #wrap :focus-visible {
45
+ outline: 2px solid var(--purple);
46
+ outline-offset: -2px;
47
+ border-radius: 2px;
48
+ }
49
+ /* Numbers line up only if the digits are the same width. Durations sit in a
50
+ right-aligned column, and proportional digits are what made its left edge
51
+ ragged from row to row. */
52
+ .num, .dur, .meth, .stat .sval, .tlbl { font-variant-numeric: tabular-nums; }
53
+ /* ── utility colours ──────────────────────────────────────────────────────── */
54
+ .green { color: var(--green); }
55
+ .yellow { color: var(--yellow); }
56
+ .red { color: var(--red); }
57
+ .cyan { color: var(--cyan); }
58
+ .dim { color: var(--muted); }
59
+ /* ── bar ──────────────────────────────────────────────────────────────────── */
60
+ #bar {
61
+ height: 32px; background: var(--bg); border-top: 1px solid var(--bdr);
62
+ display: flex; align-items: center; gap: 5px; padding: 0 8px;
63
+ cursor: pointer; user-select: none; overflow: hidden; flex-shrink: 0;
64
+ }
65
+ .logo { color: var(--purple); font-weight: 700; font-size: 13px; flex-shrink: 0; }
66
+ .dot { font-size: 8px; }
67
+ .dot.ok { color: var(--green); }
68
+ .dot.err { color: var(--red); animation: blink 1s step-start infinite; }
69
+ @keyframes blink { 50% { opacity: 0.25; } }
70
+ .bdiv { color: var(--bdr); flex-shrink: 0; }
71
+ .sp { flex: 1; }
72
+ .meth { font-weight: 700; font-size: 11px; flex-shrink: 0; }
73
+ .meth.get { color: var(--green); }
74
+ .meth.post { color: var(--cyan); }
75
+ .meth.put, .meth.patch { color: var(--yellow); }
76
+ .meth.delete { color: var(--red); }
77
+ /* Not an HTTP method: a Flow action, which arrives over the socket against a
78
+ synthetic GET of its own page. Its own colour so the list does not read as two
79
+ loads of that page. */
80
+ .meth.flow { color: var(--purple); }
81
+ .bpath { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
82
+ .sc { font-weight: 700; font-size: 11px; flex-shrink: 0; }
83
+ .sc.ok { color: var(--green); }
84
+ .sc.redir { color: var(--cyan); }
85
+ .sc.cli { color: var(--yellow); }
86
+ .sc.srv { color: var(--red); }
87
+ .chip {
88
+ font-size: 10px; padding: 1px 5px; border-radius: 999px;
89
+ background: var(--card); border: 1px solid var(--bdr); white-space: nowrap; flex-shrink: 0;
90
+ }
91
+ .chip.warn { border-color: var(--yellow); color: var(--yellow); }
92
+ .chip.ok { border-color: var(--green); color: var(--green); }
93
+ .ibtn {
94
+ height: 22px; padding: 0 6px;
95
+ background: var(--card); border: 1px solid var(--bdr); color: var(--text);
96
+ cursor: pointer; border-radius: 4px; font-size: 11px; font-family: inherit;
97
+ display: flex; align-items: center; gap: 3px; flex-shrink: 0;
98
+ }
99
+ .ibtn:hover { background: var(--surf); border-color: var(--purple); }
100
+ .ibtn.live-on { border-color: var(--green); color: var(--green); }
101
+ /* New traces arrived while pinned — an offer, never a jump. */
102
+ .ibtn.pending { border-color: var(--purple); color: var(--purple); animation: pulse 2s ease-in-out infinite; }
103
+ @keyframes pulse { 50% { opacity: 0.55; } }
104
+ /* ── panel ────────────────────────────────────────────────────────────────── */
105
+ #panel {
106
+ height: 380px; background: var(--bg); border-top: 1px solid var(--bdr);
107
+ display: flex; flex-direction: column; position: relative;
108
+ }
109
+ /* Drag strip. Sits above the tab row and overhangs upward so it is grabbable
110
+ without stealing clicks from the host page's last few pixels. */
111
+ #grip {
112
+ position: absolute; top: -3px; left: 0; right: 0; height: 7px;
113
+ cursor: ns-resize; z-index: 2;
114
+ }
115
+ #grip:hover, #grip.dragging { background: var(--purple); opacity: .5; }
116
+ #tabs {
117
+ display: flex; gap: 2px; padding: 4px 8px 0;
118
+ background: var(--surf); border-bottom: 1px solid var(--bdr);
119
+ flex-shrink: 0; overflow-x: auto;
120
+ }
121
+ .tab {
122
+ height: 28px; padding: 0 10px; background: transparent; border: none;
123
+ border-bottom: 2px solid transparent; color: var(--muted); cursor: pointer;
124
+ font: inherit; font-size: 11px; white-space: nowrap;
125
+ display: flex; align-items: center; gap: 4px;
126
+ }
127
+ .tab:hover { color: var(--text); }
128
+ .tab.active { color: var(--text); border-bottom-color: var(--purple); }
129
+ .tbdg {
130
+ font-size: 10px; padding: 0 4px; border-radius: 999px;
131
+ background: var(--card); min-width: 16px; text-align: center;
132
+ }
133
+ .tbdg.warn { background: transparent; color: var(--yellow); }
134
+ .ldot { font-size: 8px; color: var(--green); animation: blink 1.5s step-start infinite; }
135
+ #content { flex: 1; overflow-y: auto; overflow-x: hidden; position: relative; }
136
+ /* ── content shared ───────────────────────────────────────────────────────── */
137
+ .empty { color: var(--muted); text-align: center; padding: 40px 20px; }
138
+ .sec { padding: 10px 12px; border-bottom: 1px solid var(--bdr); }
139
+ .sec:last-child { border-bottom: none; }
140
+ .stitle {
141
+ font-size: 10px; text-transform: uppercase; letter-spacing: .6px;
142
+ color: var(--muted); margin-bottom: 8px;
143
+ display: flex; align-items: center; gap: 6px;
144
+ }
145
+ .stats { display: flex; flex-wrap: wrap; gap: 1px; background: var(--bdr); border-bottom: 1px solid var(--bdr); }
146
+ .stat { flex: 1; min-width: 90px; padding: 8px 12px; background: var(--bg); }
147
+ .slbl { font-size: 10px; color: var(--muted); margin-bottom: 2px; }
148
+ .sval { font-weight: 700; font-size: 13px; }
149
+ .rcard { padding: 8px 12px; border-bottom: 1px solid var(--bdr); background: var(--surf); font-size: 12px; }
150
+ /* ── copy buttons ─────────────────────────────────────────────────────────── */
151
+ .cpy {
152
+ background: transparent; border: 1px solid transparent; color: var(--muted);
153
+ font: inherit; font-size: 10px; line-height: 1; padding: 1px 4px;
154
+ border-radius: 3px; cursor: pointer; flex-shrink: 0; opacity: 0;
155
+ transition: opacity .1s;
156
+ }
157
+ .cpy:hover { color: var(--purple); border-color: var(--bdr); }
158
+ .cpy.done { color: var(--green); opacity: 1; }
159
+ .qrow:hover .cpy, .lrow:hover .cpy, .sec:hover .cpy, .tleaf:hover .cpy,
160
+ .tbranch > summary:hover .cpy, .crow:hover .cpy, .cpy:focus { opacity: 1; }
161
+ /* ── queries ──────────────────────────────────────────────────────────────── */
162
+ .qrow { padding: 8px 12px; border-bottom: 1px solid var(--bdr); }
163
+ .qrow:last-child { border-bottom: none; }
164
+ .qmeta { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
165
+ .qdur { font-weight: 700; font-size: 11px; min-width: 44px; }
166
+ .qbar { flex: 1; height: 4px; background: var(--card); border-radius: 2px; max-width: 120px; }
167
+ .qfill { height: 100%; background: var(--purple); border-radius: 2px; }
168
+ .qsql { font-size: 11px; white-space: pre-wrap; word-break: break-all; }
169
+ .qbind { font-size: 10px; color: var(--muted); margin-top: 3px; }
170
+ .bind { color: var(--orange); }
171
+ .wrow { padding: 8px 12px; border-bottom: 1px solid var(--bdr); border-left: 3px solid var(--yellow); }
172
+ .whead { color: var(--yellow); margin-bottom: 4px; font-size: 12px; }
173
+ .wfix { font-size: 10px; color: var(--muted); margin-top: 5px; }
174
+ .wfix code {
175
+ font: inherit; color: var(--cyan); background: var(--card);
176
+ padding: 1px 4px; border-radius: 3px;
177
+ }
178
+ /* ── exception banner ─────────────────────────────────────────────────────── */
179
+ .exc {
180
+ padding: 9px 12px; background: var(--card); border-left: 3px solid var(--red);
181
+ border-bottom: 1px solid var(--bdr);
182
+ }
183
+ .exhead { color: var(--red); font-weight: 700; font-size: 11px; margin-bottom: 3px; display: flex; align-items: center; gap: 6px; }
184
+ .exmsg { font-size: 11px; white-space: pre-wrap; word-break: break-word; }
185
+ /* ── logs ─────────────────────────────────────────────────────────────────── */
186
+ .lrow { display: flex; gap: 6px; padding: 4px 12px; border-bottom: 1px solid var(--bdr); }
187
+ .ltime { color: var(--muted); min-width: 54px; font-size: 10px; }
188
+ .llvl { min-width: 38px; font-weight: 700; font-size: 11px; }
189
+ .llvl.log, .llvl.debug, .llvl.info { color: var(--cyan); }
190
+ .llvl.warn { color: var(--yellow); }
191
+ .llvl.error { color: var(--red); }
192
+ .lmsg { font-size: 11px; white-space: pre-wrap; word-break: break-all; flex: 1; }
193
+ /* ── request kv table ─────────────────────────────────────────────────────── */
194
+ .kv { width: 100%; border-collapse: collapse; font-size: 11px; }
195
+ .kv td { padding: 4px 12px; border-bottom: 1px solid var(--bdr); vertical-align: top; }
196
+ .kv td:first-child { color: var(--muted); min-width: 160px; font-size: 10px; }
197
+ /* ── cards (mail / jobs) ──────────────────────────────────────────────────── */
198
+ .card { padding: 9px 12px; border-bottom: 1px solid var(--bdr); }
199
+ /* ── mail preview ─────────────────────────────────────────────────────────── */
200
+ .mprev { margin-top: 7px; }
201
+ .mprev > summary {
202
+ cursor: pointer; font-size: 10px; color: var(--muted);
203
+ list-style: none; user-select: none; width: fit-content;
204
+ }
205
+ .mprev > summary::-webkit-details-marker { display: none; }
206
+ .mprev > summary::before { content: '▸ '; }
207
+ .mprev[open] > summary::before { content: '▾ '; }
208
+ .mprev > summary:hover { color: var(--purple); }
209
+ .mframe {
210
+ width: 100%; height: 320px; margin-top: 6px; border: 1px solid var(--bdr);
211
+ border-radius: 4px; background: #fff;
212
+ }
213
+ /* ── all-requests list ────────────────────────────────────────────────────── */
214
+ /* Fixed height: the virtualiser positions rows arithmetically above ~200 of
215
+ them, and a row that can grow would put every offset out. */
216
+ .hrow {
217
+ display: flex; align-items: center; gap: 6px;
218
+ padding: 0 12px; height: 26px; border-bottom: 1px solid var(--bdr);
219
+ cursor: pointer; font-size: 11px; overflow: hidden;
220
+ }
221
+ .hrow:hover { background: var(--surf); }
222
+ .hrow.cur { background: var(--card); }
223
+ .hrow.on { box-shadow: inset 2px 0 0 var(--purple); }
224
+ .hrow.err { border-left: 3px solid var(--red); padding-left: 9px; }
225
+ .hrow.child { padding-left: 26px; background: var(--childbg); }
226
+ .hpath { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
227
+ .hexc { color: var(--red); font-size: 10px; max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
228
+ .gtog {
229
+ font-size: 10px; padding: 0 5px; border-radius: 999px; cursor: pointer;
230
+ background: var(--card); border: 1px solid var(--purple); color: var(--purple);
231
+ font-family: inherit; flex-shrink: 0;
232
+ }
233
+ .gtog:hover { background: var(--surf); }
234
+ /* Windowed rendering: two spacers stand in for the rows that are not drawn. */
235
+ .vpad { flex-shrink: 0; }
236
+ /* ── filter bar ───────────────────────────────────────────────────────────── */
237
+ .fbar {
238
+ display: flex; gap: 6px; align-items: center; padding: 6px 12px;
239
+ border-bottom: 1px solid var(--bdr); background: var(--surf);
240
+ position: sticky; top: 0; z-index: 1;
241
+ }
242
+ .finput {
243
+ flex: 1; min-width: 90px; height: 24px; padding: 0 8px; font: inherit; font-size: 11px;
244
+ background: var(--bg); border: 1px solid var(--bdr); border-radius: 4px; color: var(--text);
245
+ }
246
+ .finput:focus { outline: none; border-color: var(--purple); }
247
+ .finput::placeholder { color: var(--muted); }
248
+ .facets {
249
+ display: flex; gap: 4px; align-items: center; flex-wrap: wrap;
250
+ padding: 5px 12px; border-bottom: 1px solid var(--bdr); background: var(--surf);
251
+ }
252
+ .fchip {
253
+ font: inherit; font-size: 10px; padding: 1px 6px; border-radius: 999px; cursor: pointer;
254
+ background: transparent; border: 1px solid var(--bdr); color: var(--muted);
255
+ }
256
+ .fchip:hover { color: var(--text); border-color: var(--muted); }
257
+ .fchip.on { background: var(--card); color: var(--purple); border-color: var(--purple); }
258
+ .fchip.on.warn { color: var(--yellow); border-color: var(--yellow); }
259
+ .fsep { width: 1px; height: 12px; background: var(--bdr); margin: 0 3px; }
260
+ /* ── timeline waterfall ───────────────────────────────────────────────────── */
261
+ .trow { display: flex; align-items: center; gap: 8px; padding: 3px 12px; font-size: 11px; }
262
+ .trow:hover { background: var(--surf); }
263
+ .tlbl { min-width: 78px; font-size: 10px; text-align: right; flex-shrink: 0; }
264
+ .ttrack { flex: 1; height: 12px; position: relative; background: var(--card); border-radius: 2px; }
265
+ .tmark {
266
+ position: absolute; top: 0; height: 12px; min-width: 3px; border-radius: 2px;
267
+ background: var(--purple);
268
+ }
269
+ .tmark.query { background: var(--purple); }
270
+ .tmark.cache { background: var(--cyan); }
271
+ .tmark.mail { background: var(--green); }
272
+ .tmark.job { background: var(--orange); }
273
+ .tmark.log { background: var(--muted); }
274
+ .tmark.warn { background: var(--red); }
275
+ .tmark.chan { background: var(--yellow); }
276
+ .ttxt { flex: 2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 10px; }
277
+ .tkey { display: flex; gap: 10px; flex-wrap: wrap; padding: 6px 12px; font-size: 10px; color: var(--muted); }
278
+ /* position:static undoes .tmark, which the legend swatch reuses for its colour.
279
+ .tmark is absolutely positioned for the waterfall, and in the legend — whose
280
+ rows are not positioned — that sent every swatch to the nearest positioned
281
+ ancestor, leaving seven squares stacked above their own labels. */
282
+ .tkey i { position: static; display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 3px; vertical-align: middle; font-style: normal; }
283
+ /* ── an open request ──────────────────────────────────────────────────────── */
284
+ .lvhead {
285
+ display: flex; align-items: baseline; gap: 8px; padding: 7px 12px;
286
+ border-bottom: 1px solid var(--bdr); background: var(--surf);
287
+ position: sticky; top: 0; z-index: 1;
288
+ }
289
+ .hchev { width: 12px; flex-shrink: 0; color: var(--muted); text-align: center; }
290
+ .hdetail {
291
+ background: var(--childbg); border-bottom: 1px solid var(--bdr);
292
+ border-left: 2px solid var(--purple);
293
+ }
294
+ /* The request's own tab strip — alternatives, not a stack you scroll past. */
295
+ .dsecs {
296
+ display: flex; gap: 2px; align-items: center; flex-wrap: wrap;
297
+ padding: 4px 10px; border-bottom: 1px solid var(--bdr); background: var(--surf);
298
+ }
299
+ .dsect {
300
+ display: inline-flex; align-items: center; gap: 4px;
301
+ background: none; border: 0; border-radius: 3px; cursor: pointer;
302
+ padding: 3px 7px; font: inherit; font-size: 11px; color: var(--muted);
303
+ }
304
+ .dsect:hover { color: var(--text); background: var(--card); }
305
+ .dsect.on { color: var(--text); background: var(--card); font-weight: 700; }
306
+ .tbtn {
307
+ background: none; border: 1px solid var(--bdr); border-radius: 3px;
308
+ color: var(--muted); cursor: pointer; padding: 2px 8px; font: inherit; font-size: 11px;
309
+ flex-shrink: 0;
310
+ }
311
+ .tbtn:hover { color: var(--text); border-color: var(--muted); }
312
+ .dsec-n {
313
+ background: var(--card); color: var(--text); border-radius: 6px;
314
+ padding: 0 5px; font-size: 10px; font-variant-numeric: tabular-nums;
315
+ }
316
+ .dsec-n.warn { background: var(--red); color: var(--bg); }
317
+ /* ── generic channel rows ─────────────────────────────────────────────────── */
318
+ .crow { padding: 7px 12px; border-bottom: 1px solid var(--bdr); }
319
+ .crow.warn { border-left: 3px solid var(--yellow); }
320
+ .chead { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; }
321
+ .cttl { font-size: 11px; word-break: break-word; }
322
+ .cmeta { font-size: 10px; color: var(--muted); display: flex; gap: 10px; flex-wrap: wrap; }
323
+ /* Badge accents. Assigned by hashing the badge's own text, so repeated values
324
+ stay the same colour and devtools needs no vocabulary to tell them apart. */
325
+ .chip.a0 { border-color: var(--purple); color: var(--purple); }
326
+ .chip.a1 { border-color: var(--cyan); color: var(--cyan); }
327
+ .chip.a2 { border-color: var(--green); color: var(--green); }
328
+ .chip.a3 { border-color: var(--orange); color: var(--orange); }
329
+ .chip.a4 { border-color: var(--yellow); color: var(--yellow); }
330
+ .chip.a5 { border-color: var(--red); color: var(--red); }
331
+ .chip.flag { font-size: 9px; padding: 0 4px; text-transform: uppercase; letter-spacing: .4px; }
332
+ /* ── channel: tree ────────────────────────────────────────────────────────── */
333
+ .tnode { border-bottom: 1px solid var(--bdr); }
334
+ .tbranch > summary {
335
+ padding: 5px 12px; cursor: pointer; list-style: none; user-select: none;
336
+ display: flex; align-items: center; gap: 6px; font-size: 11px;
337
+ }
338
+ .tbranch > summary::-webkit-details-marker { display: none; }
339
+ .tbranch > summary::before { content: '▸'; color: var(--muted); font-size: 9px; }
340
+ .tbranch[open] > summary::before { content: '▾'; }
341
+ .tbranch > summary:hover { background: var(--surf); }
342
+ .tleaf { padding: 5px 12px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 11px; }
343
+ .tleaf:hover { background: var(--surf); }
344
+ .tname { font-weight: 600; }
345
+ .tattr { font-size: 10px; color: var(--muted); }
346
+ .tkids { border-left: 1px solid var(--bdr); margin-left: 17px; }
347
+ /* ── channel: table ───────────────────────────────────────────────────────── */
348
+ .ctbl { width: 100%; border-collapse: collapse; font-size: 11px; }
349
+ .ctbl th {
350
+ text-align: left; padding: 5px 12px; font-size: 10px; font-weight: 400;
351
+ text-transform: uppercase; letter-spacing: .5px; color: var(--muted);
352
+ background: var(--surf); border-bottom: 1px solid var(--bdr);
353
+ position: sticky; top: 0;
354
+ }
355
+ .ctbl td { padding: 5px 12px; border-bottom: 1px solid var(--bdr); vertical-align: top; }
356
+ .ctbl tr.warn td:first-child { box-shadow: inset 3px 0 0 var(--yellow); }
357
+ /* ── channel: grouped ─────────────────────────────────────────────────────── */
358
+ .cgrp > summary {
359
+ padding: 6px 12px; cursor: pointer; list-style: none; user-select: none;
360
+ background: var(--surf); border-bottom: 1px solid var(--bdr);
361
+ display: flex; align-items: center; gap: 8px; font-size: 11px;
362
+ }
363
+ .cgrp > summary::-webkit-details-marker { display: none; }
364
+ .cgrp > summary::before { content: '▸'; color: var(--muted); font-size: 9px; }
365
+ .cgrp[open] > summary::before { content: '▾'; }
366
+ /* ── section switcher ─────────────────────────────────────────────────────── */
367
+ .sects { display: flex; gap: 1px; align-self: center; flex-shrink: 0; margin-bottom: 4px; }
368
+ .sect {
369
+ font: inherit; font-size: 10px; padding: 3px 9px; cursor: pointer;
370
+ background: var(--bg); border: 1px solid var(--bdr); color: var(--muted);
371
+ text-transform: uppercase; letter-spacing: .5px;
372
+ }
373
+ .sect:first-child { border-radius: 4px 0 0 4px; }
374
+ .sect:last-child { border-radius: 0 4px 4px 0; }
375
+ .sect:hover { color: var(--text); }
376
+ .sect.on { background: var(--card); color: var(--purple); border-color: var(--purple); }
377
+ .tabdiv { width: 1px; background: var(--bdr); align-self: stretch; margin: 4px 6px 0; flex-shrink: 0; }
378
+ a.link { color: var(--cyan); text-decoration: none; }
379
+ a.link:hover { text-decoration: underline; }
380
+ /* ── source locations ─────────────────────────────────────────────────────── */
381
+ .src {
382
+ font-size: 10px; color: var(--muted); flex-shrink: 0; white-space: nowrap;
383
+ max-width: 220px; overflow: hidden; text-overflow: ellipsis;
384
+ }
385
+ a.src.link { color: var(--cyan); text-decoration: none; }
386
+ a.src.link:hover { text-decoration: underline; }
387
+ /* ── exception frames ─────────────────────────────────────────────────────── */
388
+ .frame {
389
+ display: flex; align-items: baseline; gap: 8px; justify-content: space-between;
390
+ padding: 4px 12px; border-bottom: 1px solid var(--bdr); font-size: 11px;
391
+ }
392
+ .frame:hover { background: var(--surf); }
393
+ /* Framework frames are context, not the answer — dimmed so your own code stands
394
+ out of a forty-frame trace without being hidden from it. */
395
+ .frame.vendor { opacity: .5; }
396
+ .frame .src { max-width: 60%; }
397
+ .fnname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
398
+ /* ── chip rows (session keys) ─────────────────────────────────────────────── */
399
+ .chips { display: flex; gap: 4px; flex-wrap: wrap; }
400
+ /* ── keyboard help ────────────────────────────────────────────────────────── */
401
+ .keys { padding: 6px 12px; font-size: 10px; color: var(--muted); display: flex; gap: 12px; flex-wrap: wrap; border-top: 1px solid var(--bdr); }
402
+ .keys kbd {
403
+ font: inherit; background: var(--card); border: 1px solid var(--bdr);
404
+ border-radius: 3px; padding: 0 3px; color: var(--text);
405
+ }
406
+ /* ── standalone dashboard ─────────────────────────────────────────────────── */
407
+ #wrap.standalone { display: flex; flex-direction: column; height: 100vh; background: var(--bg); }
408
+ #wrap.standalone #panel { flex: 1; height: auto !important; border-top: none; }
409
+ #wrap.standalone #grip { display: none; }
410
+ #wrap.standalone #bar { border-top: none; border-bottom: 1px solid var(--bdr); cursor: default; order: -1; }
411
+ </style>`;
412
+
413
+ /** What the user asked for, which is not the same as which palette is showing. */
414
+ export type ThemeChoice = "auto" | "dark" | "light";
415
+
416
+ /**
417
+ * Resolve the three-way choice to the one class the stylesheet understands.
418
+ *
419
+ * `auto` asks the browser, and the panel re-asks whenever the system flips, so a
420
+ * machine that switches at sunset does not leave a dark panel on a light page.
421
+ */
422
+ export function isLightTheme(choice: ThemeChoice): boolean {
423
+ if (choice === "light") return true;
424
+ if (choice === "dark") return false;
425
+ try {
426
+ return window.matchMedia("(prefers-color-scheme: light)").matches;
427
+ } catch {
428
+ // No matchMedia (a very old browser, a test harness) — dark is the default
429
+ // the panel has always had.
430
+ return false;
431
+ }
432
+ }
433
+
434
+ /** The next choice in the toggle's cycle, and the icon that stands for it. */
435
+ export const THEME_CYCLE: Record<ThemeChoice, ThemeChoice> = {
436
+ auto: "dark",
437
+ dark: "light",
438
+ light: "auto",
439
+ };
440
+
441
+ export const THEME_ICON: Record<ThemeChoice, string> = {
442
+ auto: "◐",
443
+ dark: "●",
444
+ light: "○",
445
+ };
@@ -4,6 +4,6 @@
4
4
  * injected into dev HTML responses by the core dev injector — so apps get the
5
5
  * floating panel with no `DevTools.start()` call in their own bundle.
6
6
  */
7
- import { DevTools } from "./client.ts";
7
+ import { DevTools } from "./client/index.ts";
8
8
 
9
9
  DevTools.start();
package/src/config.ts CHANGED
@@ -1,7 +1,35 @@
1
1
  import { deepMerge } from "@zerotal/core";
2
2
  import type { RedactionOptions } from "./redaction.ts";
3
+ import type { EditorName } from "./editor.ts";
4
+
5
+ /**
6
+ * Decides whether a request may reach the inspector outside a development
7
+ * process. Returning anything but `true` is a refusal.
8
+ */
9
+ export type DevtoolsGate = (request: Request) => boolean | Promise<boolean>;
3
10
 
4
11
  export interface DevtoolsConfigShape {
12
+ /**
13
+ * Whether the inspector runs at all.
14
+ *
15
+ * `null` (the default) follows the same dev-surface gate as the stack-trace
16
+ * error page: on under `zt dev`, off in a deployed process. `true` or `false`
17
+ * decides explicitly — and `true` outside development requires a {@link gate},
18
+ * because the absence of one is a refusal rather than a default allow.
19
+ */
20
+ enabled: boolean | null;
21
+ /**
22
+ * Who may read the inspector outside a development process.
23
+ *
24
+ * Never consulted on a development machine: a gate that can lock you out of
25
+ * your own laptop is a gate that gets switched off, and then nothing is gated.
26
+ * One function answers for every endpoint — the stream, the trace JSON, the
27
+ * dashboard, and the panel bundle are the same secret.
28
+ *
29
+ * @example
30
+ * gate: (request) => request.headers.get("X-Debug-Key") === Bun.env["DEBUG_KEY"],
31
+ */
32
+ gate: DevtoolsGate | null;
5
33
  /**
6
34
  * How many request traces to keep in memory and reload on start.
7
35
  * Default: `100`.
@@ -19,21 +47,67 @@ export interface DevtoolsConfigShape {
19
47
  */
20
48
  pruneHours: number;
21
49
  /**
22
- * Whether query bindings are masked before a trace leaves the process.
50
+ * Whether sensitive values are masked before a trace leaves the process.
23
51
  *
24
52
  * On by default: a trace is streamed to the browser *and* written to disk for
25
- * a day, and bindings are the request's real values — the password on a
53
+ * a day, and what it carries is the request's real values — the password on a
26
54
  * registration, a reset token, every customer email a listing selects by.
27
55
  * Turn it off only when you are debugging the values themselves.
28
56
  */
29
57
  redact: RedactionOptions;
58
+ /**
59
+ * Which editor `file:line` links open.
60
+ *
61
+ * Every location the panel shows becomes a link: a query's call site, a log
62
+ * line's, a stack frame, a prop's render source. Going from "this query is
63
+ * slow" to the line that ran it is the most frequent move in a debugging
64
+ * session, and it was two manual searches.
65
+ *
66
+ * Default: `vscode`. Set to `null` to render locations as plain text.
67
+ */
68
+ editor: EditorName | null;
69
+ /**
70
+ * Rewrite a captured path before it becomes a link — for editing on a machine
71
+ * that is not the one running the code.
72
+ *
73
+ * Keys are path prefixes as the *server* sees them; values are what your editor
74
+ * should open instead. A container reporting `/app/src/Foo.ts` maps home with
75
+ * `{ "/app": "/Users/you/project" }`.
76
+ */
77
+ editorPathMap: Record<string, string>;
78
+ /**
79
+ * Capture the application call site for each query and log line.
80
+ *
81
+ * "Which of my 40 queries is slow" was answerable; "where do I go to fix it"
82
+ * was not. One stack walk per recorded event, filtered to application frames —
83
+ * measured at roughly two microseconds regardless of stack depth, so about
84
+ * 0.08ms on a request running forty queries.
85
+ *
86
+ * Default: on. It only ever runs when the inspector itself is running.
87
+ */
88
+ captureSource: boolean;
89
+ /**
90
+ * Extra request headers to record, on top of the built-in safe list.
91
+ *
92
+ * The allowlist exists because a trace is persisted: `cookie` and
93
+ * `authorization` are the request's credentials. This opens up the ones you
94
+ * are actually debugging — matched case-insensitively, and `"*"` records every
95
+ * header except those `redact` masks.
96
+ */
97
+ headers: string[];
30
98
  }
31
99
 
32
100
  const defaults: DevtoolsConfigShape = {
101
+ enabled: null,
102
+ gate: null,
33
103
  capacity: 100,
34
104
  dbPath: Bun.env["ZT_DEVTOOLS_DB"] ?? ".zerotal/devtools.sqlite",
35
105
  pruneHours: Number(Bun.env["ZT_DEVTOOLS_PRUNE_HOURS"] ?? 24),
36
106
  redact: { enabled: true, allow: [], deny: [] },
107
+ editor: "vscode",
108
+ editorPathMap: {},
109
+ captureSource: true,
110
+ headers: [],
37
111
  };
38
112
 
39
113
  /**
@@ -45,6 +119,7 @@ const defaults: DevtoolsConfigShape = {
45
119
  *
46
120
  * export default DevtoolsConfig({
47
121
  * capacity: 250,
122
+ * editor: 'cursor',
48
123
  * redact: { allow: ['email', 'slug'] },
49
124
  * });
50
125
  */
@@ -8,6 +8,6 @@
8
8
  * shadowed and never gained the plugin tabs the panel had. Sharing the renderer
9
9
  * means a tab added anywhere shows up in both.
10
10
  */
11
- import { DevTools } from "./client.ts";
11
+ import { DevTools } from "./client/index.ts";
12
12
 
13
13
  DevTools.start({ mode: "standalone" });