@kitlangton/motel 0.2.4 → 0.2.6

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 (66) hide show
  1. package/AGENTS.md +23 -8
  2. package/README.md +13 -2
  3. package/package.json +35 -19
  4. package/skills/motel-debug/SKILL.md +203 -0
  5. package/skills/motel-debug/references/effect.md +38 -0
  6. package/src/App.tsx +12 -5
  7. package/src/StartupGate.tsx +289 -0
  8. package/src/cli.ts +15 -16
  9. package/src/config.ts +7 -1
  10. package/src/daemon.test.ts +332 -51
  11. package/src/daemon.ts +105 -153
  12. package/src/httpApi.ts +1 -0
  13. package/src/httpListPolicy.test.ts +76 -0
  14. package/src/httpListPolicy.ts +129 -0
  15. package/src/index.tsx +9 -2
  16. package/src/localServer.ts +194 -313
  17. package/src/mcp.ts +2 -1
  18. package/src/motel.ts +0 -2
  19. package/src/opentui-jsx.d.ts +11 -0
  20. package/src/otlp.test.ts +65 -0
  21. package/src/otlp.ts +20 -0
  22. package/src/otlpProtobuf.ts +35 -0
  23. package/src/registry.ts +37 -11
  24. package/src/runtime.ts +2 -6
  25. package/src/services/AsyncIngest.ts +22 -8
  26. package/src/services/LogQueryService.ts +13 -27
  27. package/src/services/TelemetryQuery.ts +62 -0
  28. package/src/services/TelemetryStore.ts +546 -231
  29. package/src/services/TraceQueryService.ts +22 -56
  30. package/src/services/ingestRpc.ts +2 -4
  31. package/src/services/queryRpc.ts +15 -0
  32. package/src/services/telemetryQueryWorker.ts +32 -0
  33. package/src/services/telemetryWorker.ts +5 -8
  34. package/src/startupBench.ts +19 -0
  35. package/src/storybook/aiChatStory.tsx +1 -1
  36. package/src/telemetry.test.ts +307 -41
  37. package/src/ui/AiChatView.tsx +1 -1
  38. package/src/ui/AttrFilterModal.tsx +1 -1
  39. package/src/ui/ServiceLogs.tsx +10 -7
  40. package/src/ui/SpanContentView.tsx +24 -21
  41. package/src/ui/TraceDetailsPane.tsx +1 -1
  42. package/src/ui/TraceList.tsx +1 -1
  43. package/src/ui/aiState.ts +10 -22
  44. package/src/ui/app/TraceWorkspace.tsx +2 -1
  45. package/src/ui/app/useAppLayout.ts +1 -1
  46. package/src/ui/app/useTraceScreenData.ts +35 -23
  47. package/src/ui/atoms.ts +1 -1
  48. package/src/ui/cachedLoader.test.ts +23 -0
  49. package/src/ui/cachedLoader.ts +60 -0
  50. package/src/ui/loaders.ts +34 -53
  51. package/src/ui/persistence.ts +3 -3
  52. package/src/ui/primitives.tsx +1 -1
  53. package/src/ui/state.ts +2 -0
  54. package/src/ui/theme.ts +7 -5
  55. package/src/ui/traceDetailsWidth.repro.test.ts +12 -1
  56. package/src/ui/traceSortNav.repro.seed.ts +1 -1
  57. package/src/ui/traceSortNav.repro.test.ts +12 -2
  58. package/src/ui/useAttrFilterPicker.ts +10 -8
  59. package/src/ui/useKeyboardNav.ts +28 -5
  60. package/src/ui/waterfallNav.repro.seed.ts +1 -1
  61. package/src/ui/waterfallNav.repro.test.ts +16 -8
  62. package/web/dist/assets/index-B01z9BaO.css +2 -0
  63. package/web/dist/assets/index-M86tcih5.js +22 -0
  64. package/web/dist/index.html +2 -2
  65. package/web/dist/assets/index-DnyVo03x.js +0 -27
  66. package/web/dist/assets/index-DzuHNBGV.css +0 -2
@@ -57,6 +57,6 @@ const resourceSpans = specs.map((spec) => ({
57
57
  }))
58
58
 
59
59
  await storeRuntime.runPromise(
60
- Effect.flatMap(TelemetryStore.asEffect(), (store) => store.ingestTraces({ resourceSpans })),
60
+ Effect.flatMap(TelemetryStore, (store) => store.ingestTraces({ resourceSpans })),
61
61
  )
62
62
  process.exit(0)
@@ -93,6 +93,8 @@ const SERVICE_NAME = "sort-nav-repro"
93
93
  describe("trace navigation after changing sort", () => {
94
94
  const tempDir = mkdtempSync(join(tmpdir(), "motel-sort-repro-"))
95
95
  const dbPath = join(tempDir, "telemetry.sqlite")
96
+ const port = 50_000 + Math.floor(Math.random() * 5_000)
97
+ const daemonEnv = { MOTEL_RUNTIME_DIR: tempDir, MOTEL_OTEL_DB_PATH: dbPath, MOTEL_OTEL_BASE_URL: `http://127.0.0.1:${port}`, MOTEL_OTEL_QUERY_URL: `http://127.0.0.1:${port}`, MOTEL_OTEL_PORT: String(port) }
96
98
  const lastServicePath = join(tempDir, "last-service.txt")
97
99
  let canRun = false
98
100
 
@@ -106,7 +108,7 @@ describe("trace navigation after changing sort", () => {
106
108
  const seed = Bun.spawn({
107
109
  cmd: ["bun", "run", "src/ui/traceSortNav.repro.seed.ts"],
108
110
  cwd: process.cwd(),
109
- env: { ...process.env, MOTEL_OTEL_DB_PATH: dbPath, MOTEL_OTEL_ENABLED: "false" },
111
+ env: { ...process.env, ...daemonEnv, MOTEL_OTEL_ENABLED: "false" },
110
112
  stdout: "pipe",
111
113
  stderr: "pipe",
112
114
  })
@@ -128,6 +130,10 @@ describe("trace navigation after changing sort", () => {
128
130
  "--rows", "20",
129
131
  "--cwd", process.cwd(),
130
132
  "--env", `MOTEL_OTEL_DB_PATH=${dbPath}`,
133
+ "--env", `MOTEL_RUNTIME_DIR=${tempDir}`,
134
+ "--env", `MOTEL_OTEL_BASE_URL=${daemonEnv.MOTEL_OTEL_BASE_URL}`,
135
+ "--env", `MOTEL_OTEL_QUERY_URL=${daemonEnv.MOTEL_OTEL_QUERY_URL}`,
136
+ "--env", `MOTEL_OTEL_PORT=${port}`,
131
137
  "--env", "MOTEL_OTEL_ENABLED=false",
132
138
  "--timeout", "15000",
133
139
  ])
@@ -137,7 +143,11 @@ describe("trace navigation after changing sort", () => {
137
143
  }, 60_000)
138
144
 
139
145
  afterAll(async () => {
140
- if (canRun) await tui(["close", "--session", SESSION])
146
+ if (canRun) {
147
+ await tui(["close", "--session", SESSION])
148
+ const stop = Bun.spawn({ cmd: ["bun", "run", "src/motel.ts", "stop"], cwd: process.cwd(), env: { ...process.env, ...daemonEnv }, stdout: "ignore", stderr: "ignore" })
149
+ await stop.exited
150
+ }
141
151
  try { rmSync(tempDir, { recursive: true, force: true }) } catch {}
142
152
  })
143
153
 
@@ -3,13 +3,15 @@ import { useEffect } from "react"
3
3
  import {
4
4
  attrFacetStateAtom,
5
5
  attrPickerModeAtom,
6
- ensureTraceAttributeKeys,
7
- ensureTraceAttributeValues,
8
- getCachedFacetKeys,
9
- getCachedFacetValues,
10
6
  initialAttrFacetState,
11
7
  selectedTraceServiceAtom,
12
- } from "./state.ts"
8
+ } from "./atoms.ts"
9
+ import {
10
+ getCachedFacetKeys,
11
+ getCachedFacetValues,
12
+ refreshTraceAttributeKeys,
13
+ refreshTraceAttributeValues,
14
+ } from "./loaders.ts"
13
15
 
14
16
  // Drive the picker's data state from (pickerMode, service, selectedKey).
15
17
  //
@@ -17,7 +19,7 @@ import {
17
19
  // module-level cache has instantly (no "loading…" flash), then kick off a
18
20
  // background revalidation. The first time we see a (service, key) tuple
19
21
  // we still show `loading` so the UI has something to say. The module-level
20
- // caches in `state.ts` mean a service-change pre-warm can fill the cache
22
+ // caches in `loaders.ts` mean a service-change pre-warm can fill the cache
21
23
  // before the user ever presses `f`.
22
24
  export const useAttrFilterPicker = (selectedKey: string | null) => {
23
25
  const [pickerMode] = useAtom(attrPickerModeAtom)
@@ -52,7 +54,7 @@ export const useAttrFilterPicker = (selectedKey: string | null) => {
52
54
  } else {
53
55
  publishLoading(null)
54
56
  }
55
- ensureTraceAttributeKeys(service)
57
+ refreshTraceAttributeKeys(service)
56
58
  .then((entry) => {
57
59
  if (cancelled) return
58
60
  publishReady(null, entry.data)
@@ -68,7 +70,7 @@ export const useAttrFilterPicker = (selectedKey: string | null) => {
68
70
  } else {
69
71
  publishLoading(selectedKey)
70
72
  }
71
- ensureTraceAttributeValues(service, selectedKey)
73
+ refreshTraceAttributeValues(service, selectedKey)
72
74
  .then((entry) => {
73
75
  if (cancelled) return
74
76
  publishReady(selectedKey, entry.data)
@@ -13,15 +13,13 @@ import {
13
13
  attrPickerInputAtom,
14
14
  attrPickerModeAtom,
15
15
  autoRefreshAtom,
16
- chatDetailChunkIdAtom,
17
- chatDetailScrollOffsetAtom,
18
16
  collapsedSpanIdsAtom,
19
17
  detailViewAtom,
20
18
  filterModeAtom,
21
19
  filterTextAtom,
20
+ initialAttrFacetState,
22
21
  refreshNonceAtom,
23
22
  selectedAttrIndexAtom,
24
- selectedChatChunkIdAtom,
25
23
  selectedThemeAtom,
26
24
  selectedServiceLogIndexAtom,
27
25
  selectedSpanIndexAtom,
@@ -34,7 +32,9 @@ import {
34
32
  traceStateAtom,
35
33
  waterfallFilterModeAtom,
36
34
  waterfallFilterTextAtom,
37
- } from "./state.ts"
35
+ } from "./atoms.ts"
36
+ import { chatDetailChunkIdAtom, chatDetailScrollOffsetAtom, selectedChatChunkIdAtom } from "./aiState.ts"
37
+ import { getCachedFacetKeys, getCachedFacetValues } from "./loaders.ts"
38
38
  import { filterFacets } from "./AttrFilterModal.tsx"
39
39
  import { G_PREFIX_TIMEOUT_MS } from "./theme.ts"
40
40
  import { cycleThemeName, themeLabel } from "./theme.ts"
@@ -125,7 +125,7 @@ export const useKeyboardNav = (params: KeyboardNavParams) => {
125
125
  const [pickerMode, setPickerMode] = useAtom(attrPickerModeAtom)
126
126
  const [pickerInput, setPickerInput] = useAtom(attrPickerInputAtom)
127
127
  const [pickerIndex, setPickerIndex] = useAtom(attrPickerIndexAtom)
128
- const [attrFacets] = useAtom(attrFacetStateAtom)
128
+ const [attrFacets, setAttrFacets] = useAtom(attrFacetStateAtom)
129
129
  const [activeAttrKey, setActiveAttrKey] = useAtom(activeAttrKeyAtom)
130
130
  const [activeAttrValue, setActiveAttrValue] = useAtom(activeAttrValueAtom)
131
131
  const [waterfallFilterMode, setWaterfallFilterMode] = useAtom(waterfallFilterModeAtom)
@@ -259,6 +259,26 @@ export const useKeyboardNav = (params: KeyboardNavParams) => {
259
259
  setPickerIndex(0)
260
260
  }
261
261
 
262
+ const hydrateCachedPickerKeys = (service: string | null) => {
263
+ if (!service) {
264
+ setAttrFacets(initialAttrFacetState)
265
+ return
266
+ }
267
+ const cached = getCachedFacetKeys(service)
268
+ if (!cached) return
269
+ setAttrFacets({ status: "ready", key: null, data: cached.data, error: null })
270
+ }
271
+
272
+ const hydrateCachedPickerValues = (service: string | null, key: string | null) => {
273
+ if (!service || !key) {
274
+ setAttrFacets(initialAttrFacetState)
275
+ return
276
+ }
277
+ const cached = getCachedFacetValues(service, key)
278
+ if (!cached) return
279
+ setAttrFacets({ status: "ready", key, data: cached.data, error: null })
280
+ }
281
+
262
282
  const closePicker = () => {
263
283
  setPickerMode("off")
264
284
  resetPicker()
@@ -587,6 +607,7 @@ export const useKeyboardNav = (params: KeyboardNavParams) => {
587
607
  const row = rows[clampedIndex]
588
608
  if (!row) return true
589
609
  if (s.pickerMode === "keys") {
610
+ hydrateCachedPickerValues(s.selectedTraceService, row.value)
590
611
  setActiveAttrKey(row.value)
591
612
  setPickerMode("values")
592
613
  resetPicker()
@@ -604,6 +625,7 @@ export const useKeyboardNav = (params: KeyboardNavParams) => {
604
625
  return true
605
626
  }
606
627
  if (s.pickerMode === "values") {
628
+ hydrateCachedPickerKeys(s.selectedTraceService)
607
629
  setPickerMode("keys")
608
630
  setActiveAttrKey(null)
609
631
  setPickerIndex(0)
@@ -857,6 +879,7 @@ export const useKeyboardNav = (params: KeyboardNavParams) => {
857
879
  return true
858
880
  }
859
881
  if ((key.name === "f" || key.name === "F") && !key.ctrl && !key.meta) {
882
+ hydrateCachedPickerKeys(s.selectedTraceService)
860
883
  setPickerMode("keys")
861
884
  resetPicker()
862
885
  setActiveAttrKey(null)
@@ -48,7 +48,7 @@ const span = (
48
48
  endTimeUnixNano: ms(endMs),
49
49
  })
50
50
 
51
- const program = Effect.flatMap(TelemetryStore.asEffect(), (store) =>
51
+ const program = Effect.flatMap(TelemetryStore, (store) =>
52
52
  store.ingestTraces({
53
53
  resourceSpans: [
54
54
  {
@@ -60,12 +60,13 @@ const press = async (...keys: string[]) => {
60
60
  // *last* pair makes the helper robust to layout changes that add or remove
61
61
  // header dividers (breadcrumbs, split-divider junctions, etc.).
62
62
  //
63
- // In wide (side-by-side) mode each line also contains the trace list on the
64
- // left half separated by `│`. The list renders relative ages like "6s / 7s"
65
- // that drift between snapshots; strip everything left of the first `│` so
66
- // only the waterfall contributes to the comparison.
63
+ // In wide level-1 mode the waterfall is rendered in the left pane and span
64
+ // detail is rendered on the right. Determine the pane separator from the
65
+ // stable header line rather than mistaking tree branch glyphs for it.
67
66
  const waterfallBody = (snap: string): readonly string[] => {
68
67
  const lines = snap.split("\n")
68
+ const header = lines.find((line) => line.includes("TRACE DETAILS") && line.includes("SPAN"))
69
+ const paneSeparator = header?.indexOf("\u2502") ?? -1
69
70
  const dividerIdxs: number[] = []
70
71
  for (let i = 0; i < lines.length; i++) {
71
72
  if (lines[i]!.startsWith("─")) dividerIdxs.push(i)
@@ -74,8 +75,7 @@ const waterfallBody = (snap: string): readonly string[] => {
74
75
  const start = dividerIdxs[dividerIdxs.length - 2]! + 1
75
76
  const end = dividerIdxs[dividerIdxs.length - 1]!
76
77
  return lines.slice(start, end).map((line) => {
77
- const barIdx = line.indexOf("\u2502")
78
- const sliced = barIdx >= 0 ? line.slice(barIdx) : line
78
+ const sliced = paneSeparator >= 0 ? line.slice(0, paneSeparator) : line
79
79
  return sliced.replace(/\s+$/g, "")
80
80
  })
81
81
  }
@@ -103,6 +103,8 @@ const SERVICE_NAME = "waterfall-repro"
103
103
  describe("waterfall collapse/expand (end-to-end TUI)", () => {
104
104
  const tempDir = mkdtempSync(join(tmpdir(), "motel-repro-"))
105
105
  const dbPath = join(tempDir, "telemetry.sqlite")
106
+ const port = 40_000 + Math.floor(Math.random() * 5_000)
107
+ const daemonEnv = { MOTEL_RUNTIME_DIR: tempDir, MOTEL_OTEL_DB_PATH: dbPath, MOTEL_OTEL_BASE_URL: `http://127.0.0.1:${port}`, MOTEL_OTEL_QUERY_URL: `http://127.0.0.1:${port}`, MOTEL_OTEL_PORT: String(port) }
106
108
  const lastServicePath = join(tempDir, "last-service.txt")
107
109
  let canRun = false
108
110
 
@@ -122,6 +124,7 @@ describe("waterfall collapse/expand (end-to-end TUI)", () => {
122
124
  cwd: process.cwd(),
123
125
  env: {
124
126
  ...process.env,
127
+ ...daemonEnv,
125
128
  MOTEL_OTEL_DB_PATH: dbPath,
126
129
  MOTEL_OTEL_RETENTION_HOURS: "24",
127
130
  MOTEL_OTEL_ENABLED: "false",
@@ -141,8 +144,7 @@ describe("waterfall collapse/expand (end-to-end TUI)", () => {
141
144
  // Make sure no stale session is hanging around with the same name.
142
145
  await tui(["close", "--session", SESSION])
143
146
 
144
- // Launch the TUI. We use a dedicated entry point (src/index.tsx) and a
145
- // generous viewport so the waterfall isn't truncated.
147
+ // Launch wide enough to exercise the waterfall + span-detail split.
146
148
  const launch = await tui([
147
149
  "launch",
148
150
  "bun run src/index.tsx",
@@ -151,6 +153,10 @@ describe("waterfall collapse/expand (end-to-end TUI)", () => {
151
153
  "--rows", "40",
152
154
  "--cwd", process.cwd(),
153
155
  "--env", `MOTEL_OTEL_DB_PATH=${dbPath}`,
156
+ "--env", `MOTEL_RUNTIME_DIR=${tempDir}`,
157
+ "--env", `MOTEL_OTEL_BASE_URL=${daemonEnv.MOTEL_OTEL_BASE_URL}`,
158
+ "--env", `MOTEL_OTEL_QUERY_URL=${daemonEnv.MOTEL_OTEL_QUERY_URL}`,
159
+ "--env", `MOTEL_OTEL_PORT=${port}`,
154
160
  "--env", "MOTEL_OTEL_ENABLED=false",
155
161
  "--timeout", "15000",
156
162
  ])
@@ -170,6 +176,8 @@ describe("waterfall collapse/expand (end-to-end TUI)", () => {
170
176
  afterAll(async () => {
171
177
  if (canRun) {
172
178
  await tui(["close", "--session", SESSION])
179
+ const stop = Bun.spawn({ cmd: ["bun", "run", "src/motel.ts", "stop"], cwd: process.cwd(), env: { ...process.env, ...daemonEnv }, stdout: "ignore", stderr: "ignore" })
180
+ await stop.exited
173
181
  }
174
182
  try {
175
183
  rmSync(tempDir, { recursive: true, force: true })
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.1 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:"Berkeley Mono", "IBM Plex Mono", "JetBrains Mono", "Fira Code", ui-monospace, monospace;--color-red-400:oklch(70.4% .191 22.216);--color-amber-400:oklch(82.8% .189 84.429);--color-emerald-400:oklch(76.5% .177 163.223);--color-sky-400:oklch(74.6% .16 232.661);--color-zinc-50:oklch(98.5% 0 0);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-900:oklch(21% .006 285.885);--color-zinc-950:oklch(14.1% .005 285.823);--color-white:#fff;--spacing:.25rem;--container-2xl:42rem;--container-3xl:48rem;--container-7xl:80rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--radius-sm:.25rem;--radius-md:.375rem;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-accent:oklch(79.5% .16 70);--color-accent-dim:oklch(65% .12 70)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-0{top:0}.top-1\/2{top:50%}.right-0{right:0}.right-1{right:var(--spacing)}.bottom-0{bottom:0}.bottom-1{bottom:var(--spacing)}.left-0{left:0}.left-1{left:var(--spacing)}.isolate{isolation:isolate}.z-10{z-index:10}.z-20{z-index:20}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-block{display:inline-block}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.h-3{height:calc(var(--spacing) * 3)}.h-full{height:100%}.min-h-0{min-height:0}.w-16{width:calc(var(--spacing) * 16)}.w-28{width:calc(var(--spacing) * 28)}.w-72{width:calc(var(--spacing) * 72)}.w-\[7rem\]{width:7rem}.w-\[440px\]{width:440px}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-28{max-width:calc(var(--spacing) * 28)}.max-w-32{max-width:calc(var(--spacing) * 32)}.min-w-0{min-width:0}.min-w-80{min-width:calc(var(--spacing) * 80)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-5{column-gap:calc(var(--spacing) * 5)}.gap-y-1{row-gap:var(--spacing)}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-accent-dim\/50{border-color:#bd813080}@supports (color:color-mix(in lab, red, red)){.border-accent-dim\/50{border-color:color-mix(in oklab, var(--color-accent-dim) 50%, transparent)}}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.border-white\/5{border-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.border-white\/\[0\.03\]{border-color:#ffffff08}@supports (color:color-mix(in lab, red, red)){.border-white\/\[0\.03\]{border-color:color-mix(in oklab, var(--color-white) 3%, transparent)}}.bg-accent\/5{background-color:#fca72c0d}@supports (color:color-mix(in lab, red, red)){.bg-accent\/5{background-color:color-mix(in oklab, var(--color-accent) 5%, transparent)}}.bg-accent\/10{background-color:#fca72c1a}@supports (color:color-mix(in lab, red, red)){.bg-accent\/10{background-color:color-mix(in oklab, var(--color-accent) 10%, transparent)}}.bg-accent\/15{background-color:#fca72c26}@supports (color:color-mix(in lab, red, red)){.bg-accent\/15{background-color:color-mix(in oklab, var(--color-accent) 15%, transparent)}}.bg-amber-400\/10{background-color:#fcbb001a}@supports (color:color-mix(in lab, red, red)){.bg-amber-400\/10{background-color:color-mix(in oklab, var(--color-amber-400) 10%, transparent)}}.bg-emerald-400\/10{background-color:#00d2941a}@supports (color:color-mix(in lab, red, red)){.bg-emerald-400\/10{background-color:color-mix(in oklab, var(--color-emerald-400) 10%, transparent)}}.bg-red-400\/10{background-color:#ff65681a}@supports (color:color-mix(in lab, red, red)){.bg-red-400\/10{background-color:color-mix(in oklab, var(--color-red-400) 10%, transparent)}}.bg-sky-400\/10{background-color:#00bcfe1a}@supports (color:color-mix(in lab, red, red)){.bg-sky-400\/10{background-color:color-mix(in oklab, var(--color-sky-400) 10%, transparent)}}.bg-transparent{background-color:#0000}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.bg-white\/10{background-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.bg-zinc-500\/10{background-color:#71717b1a}@supports (color:color-mix(in lab, red, red)){.bg-zinc-500\/10{background-color:color-mix(in oklab, var(--color-zinc-500) 10%, transparent)}}.bg-zinc-600\/10{background-color:#52525c1a}@supports (color:color-mix(in lab, red, red)){.bg-zinc-600\/10{background-color:color-mix(in oklab, var(--color-zinc-600) 10%, transparent)}}.bg-zinc-900{background-color:var(--color-zinc-900)}.bg-zinc-900\/50{background-color:#18181b80}@supports (color:color-mix(in lab, red, red)){.bg-zinc-900\/50{background-color:color-mix(in oklab, var(--color-zinc-900) 50%, transparent)}}.bg-zinc-950{background-color:var(--color-zinc-950)}.p-16{padding:calc(var(--spacing) * 16)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-20{padding-block:calc(var(--spacing) * 20)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pr-1\.5{padding-right:calc(var(--spacing) * 1.5)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-6{padding-right:calc(var(--spacing) * 6)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-1\.5{padding-left:calc(var(--spacing) * 1.5)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-6{padding-left:calc(var(--spacing) * 6)}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.text-balance{text-wrap:balance}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-accent{color:var(--color-accent)}.text-amber-400{color:var(--color-amber-400)}.text-emerald-400{color:var(--color-emerald-400)}.text-inherit{color:inherit}.text-red-400{color:var(--color-red-400)}.text-sky-400{color:var(--color-sky-400)}.text-white\/90{color:#ffffffe6}@supports (color:color-mix(in lab, red, red)){.text-white\/90{color:color-mix(in oklab, var(--color-white) 90%, transparent)}}.text-zinc-50{color:var(--color-zinc-50)}.text-zinc-100{color:var(--color-zinc-100)}.text-zinc-200{color:var(--color-zinc-200)}.text-zinc-300{color:var(--color-zinc-300)}.text-zinc-400{color:var(--color-zinc-400)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-600{color:var(--color-zinc-600)}.text-zinc-700{color:var(--color-zinc-700)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.scheme-only-dark{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark only}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.placeholder\:text-zinc-600::placeholder{color:var(--color-zinc-600)}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}@media (hover:hover){.hover\:border-white\/20:hover{border-color:#fff3}@supports (color:color-mix(in lab, red, red)){.hover\:border-white\/20:hover{border-color:color-mix(in oklab, var(--color-white) 20%, transparent)}}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.hover\:bg-white\/\[0\.03\]:hover{background-color:#ffffff08}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/\[0\.03\]:hover{background-color:color-mix(in oklab, var(--color-white) 3%, transparent)}}.hover\:text-accent:hover{color:var(--color-accent)}.hover\:text-zinc-200:hover{color:var(--color-zinc-200)}.hover\:text-zinc-300:hover{color:var(--color-zinc-300)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:border-accent-dim:focus{border-color:var(--color-accent-dim)}}html,body,#root{height:100%;font-family:var(--font-mono)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--color-white)/.1;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--color-white)/.2}@keyframes pulse-bar{0%,to{opacity:.5}50%{opacity:1}}.animate-bar-pulse{animation:1.5s ease-in-out infinite pulse-bar}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}