@oh-my-pi/omp-stats 18.2.0 → 18.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.2.1] - 2026-09-15
6
+
7
+ ### Fixed
8
+
9
+ - Fixed the sessions API returning an encoded storage key instead of the session working directory ([#12078](https://github.com/can1357/oh-my-pi/pull/12078) by [@Dante-dan](https://github.com/Dante-dan)).
10
+ - Fixed Traces search inputs and checkbox using unthemed browser defaults, and iOS viewport zoom on search focus.
11
+ - Fixed garbage tool names from provider-side parse failures (e.g. a gateway returning the model's whole invocation text as the tool name) polluting the tools dashboard's per-tool rows and filter dropdown; such names now collapse to their leading identifier, and existing databases re-ingest cleaned on next sync.
12
+ - Reduced repeat stats-sync reads to appended transcript data while preserving service-tier accounting across restarts.
13
+ - Rebuilt stats for replaced or truncated session files instead of retaining stale totals.
14
+ - Reclaimed lingering stats dashboards from older releases using their versioned HTTP identity when process command lines are unavailable, so upgrades no longer leave `omp stats` blocked by an opaque Bun listener.
15
+
5
16
  ## [18.1.17] - 2026-09-10
6
17
 
7
18
  ### Fixed
@@ -1 +1 @@
1
- @layer theme,base,components,utilities;@layer theme{@theme default{--font-sans: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; --font-serif: ui-serif,Georgia,Cambria,"Times New Roman",Times,serif; --font-mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; --color-red-50: oklch(97.1% .013 17.38); --color-red-100: oklch(93.6% .032 17.717); --color-red-200: oklch(88.5% .062 18.334); --color-red-300: oklch(80.8% .114 19.571); --color-red-400: oklch(70.4% .191 22.216); --color-red-500: oklch(63.7% .237 25.331); --color-red-600: oklch(57.7% .245 27.325); --color-red-700: oklch(50.5% .213 27.518); --color-red-800: oklch(44.4% .177 26.899); --color-red-900: oklch(39.6% .141 25.723); --color-red-950: oklch(25.8% .092 26.042); --color-orange-50: oklch(98% .016 73.684); --color-orange-100: oklch(95.4% .038 75.164); --color-orange-200: oklch(90.1% .076 70.697); --color-orange-300: oklch(83.7% .128 66.29); --color-orange-400: oklch(75% .183 55.934); --color-orange-500: oklch(70.5% .213 47.604); --color-orange-600: oklch(64.6% .222 41.116); --color-orange-700: oklch(55.3% .195 38.402); --color-orange-800: oklch(47% .157 37.304); --color-orange-900: oklch(40.8% .123 38.172); --color-orange-950: oklch(26.6% .079 36.259); --color-amber-50: oklch(98.7% .022 95.277); --color-amber-100: oklch(96.2% .059 95.617); --color-amber-200: oklch(92.4% .12 95.746); --color-amber-300: oklch(87.9% .169 91.605); --color-amber-400: oklch(82.8% .189 84.429); --color-amber-500: oklch(76.9% .188 70.08); --color-amber-600: oklch(66.6% .179 58.318); --color-amber-700: oklch(55.5% .163 48.998); --color-amber-800: oklch(47.3% .137 46.201); --color-amber-900: oklch(41.4% .112 45.904); --color-amber-950: oklch(27.9% .077 45.635); --color-yellow-50: oklch(98.7% .026 102.212); --color-yellow-100: oklch(97.3% .071 103.193); --color-yellow-200: oklch(94.5% .129 101.54); --color-yellow-300: oklch(90.5% .182 98.111); --color-yellow-400: oklch(85.2% .199 91.936); --color-yellow-500: oklch(79.5% .184 86.047); --color-yellow-600: oklch(68.1% .162 75.834); --color-yellow-700: oklch(55.4% .135 66.442); --color-yellow-800: oklch(47.6% .114 61.907); --color-yellow-900: oklch(42.1% .095 57.708); --color-yellow-950: oklch(28.6% .066 53.813); --color-lime-50: oklch(98.6% .031 120.757); --color-lime-100: oklch(96.7% .067 122.328); --color-lime-200: oklch(93.8% .127 124.321); --color-lime-300: oklch(89.7% .196 126.665); --color-lime-400: oklch(84.1% .238 128.85); --color-lime-500: oklch(76.8% .233 130.85); --color-lime-600: oklch(64.8% .2 131.684); --color-lime-700: oklch(53.2% .157 131.589); --color-lime-800: oklch(45.3% .124 130.933); --color-lime-900: oklch(40.5% .101 131.063); --color-lime-950: oklch(27.4% .072 132.109); --color-green-50: oklch(98.2% .018 155.826); --color-green-100: oklch(96.2% .044 156.743); --color-green-200: oklch(92.5% .084 155.995); --color-green-300: oklch(87.1% .15 154.449); --color-green-400: oklch(79.2% .209 151.711); --color-green-500: oklch(72.3% .219 149.579); --color-green-600: oklch(62.7% .194 149.214); --color-green-700: oklch(52.7% .154 150.069); --color-green-800: oklch(44.8% .119 151.328); --color-green-900: oklch(39.3% .095 152.535); --color-green-950: oklch(26.6% .065 152.934); --color-emerald-50: oklch(97.9% .021 166.113); --color-emerald-100: oklch(95% .052 163.051); --color-emerald-200: oklch(90.5% .093 164.15); --color-emerald-300: oklch(84.5% .143 164.978); --color-emerald-400: oklch(76.5% .177 163.223); --color-emerald-500: oklch(69.6% .17 162.48); --color-emerald-600: oklch(59.6% .145 163.225); --color-emerald-700: oklch(50.8% .118 165.612); --color-emerald-800: oklch(43.2% .095 166.913); --color-emerald-900: oklch(37.8% .077 168.94); --color-emerald-950: oklch(26.2% .051 172.552); --color-teal-50: oklch(98.4% .014 180.72); --color-teal-100: oklch(95.3% .051 180.801); --color-teal-200: oklch(91% .096 180.426); --color-teal-300: oklch(85.5% .138 181.071); --color-teal-400: oklch(77.7% .152 181.912); --color-teal-500: oklch(70.4% .14 182.503); --color-teal-600: oklch(60% .118 184.704); --color-teal-700: oklch(51.1% .096 186.391); --color-teal-800: oklch(43.7% .078 188.216); --color-teal-900: oklch(38.6% .063 188.416); --color-teal-950: oklch(27.7% .046 192.524); --color-cyan-50: oklch(98.4% .019 200.873); --color-cyan-100: oklch(95.6% .045 203.388); --color-cyan-200: oklch(91.7% .08 205.041); --color-cyan-300: oklch(86.5% .127 207.078); --color-cyan-400: oklch(78.9% .154 211.53); --color-cyan-500: oklch(71.5% .143 215.221); --color-cyan-600: oklch(60.9% .126 221.723); --color-cyan-700: oklch(52% .105 223.128); --color-cyan-800: oklch(45% .085 224.283); --color-cyan-900: oklch(39.8% .07 227.392); --color-cyan-950: oklch(30.2% .056 229.695); --color-sky-50: oklch(97.7% .013 236.62); --color-sky-100: oklch(95.1% .026 236.824); --color-sky-200: oklch(90.1% .058 230.902); --color-sky-300: oklch(82.8% .111 230.318); --color-sky-400: oklch(74.6% .16 232.661); --color-sky-500: oklch(68.5% .169 237.323); --color-sky-600: oklch(58.8% .158 241.966); --color-sky-700: oklch(50% .134 242.749); --color-sky-800: oklch(44.3% .11 240.79); --color-sky-900: oklch(39.1% .09 240.876); --color-sky-950: oklch(29.3% .066 243.157); --color-blue-50: oklch(97% .014 254.604); --color-blue-100: oklch(93.2% .032 255.585); --color-blue-200: oklch(88.2% .059 254.128); --color-blue-300: oklch(80.9% .105 251.813); --color-blue-400: oklch(70.7% .165 254.624); --color-blue-500: oklch(62.3% .214 259.815); --color-blue-600: oklch(54.6% .245 262.881); --color-blue-700: oklch(48.8% .243 264.376); --color-blue-800: oklch(42.4% .199 265.638); --color-blue-900: oklch(37.9% .146 265.522); --color-blue-950: oklch(28.2% .091 267.935); --color-indigo-50: oklch(96.2% .018 272.314); --color-indigo-100: oklch(93% .034 272.788); --color-indigo-200: oklch(87% .065 274.039); --color-indigo-300: oklch(78.5% .115 274.713); --color-indigo-400: oklch(67.3% .182 276.935); --color-indigo-500: oklch(58.5% .233 277.117); --color-indigo-600: oklch(51.1% .262 276.966); --color-indigo-700: oklch(45.7% .24 277.023); --color-indigo-800: oklch(39.8% .195 277.366); --color-indigo-900: oklch(35.9% .144 278.697); --color-indigo-950: oklch(25.7% .09 281.288); --color-violet-50: oklch(96.9% .016 293.756); --color-violet-100: oklch(94.3% .029 294.588); --color-violet-200: oklch(89.4% .057 293.283); --color-violet-300: oklch(81.1% .111 293.571); --color-violet-400: oklch(70.2% .183 293.541); --color-violet-500: oklch(60.6% .25 292.717); --color-violet-600: oklch(54.1% .281 293.009); --color-violet-700: oklch(49.1% .27 292.581); --color-violet-800: oklch(43.2% .232 292.759); --color-violet-900: oklch(38% .189 293.745); --color-violet-950: oklch(28.3% .141 291.089); --color-purple-50: oklch(97.7% .014 308.299); --color-purple-100: oklch(94.6% .033 307.174); --color-purple-200: oklch(90.2% .063 306.703); --color-purple-300: oklch(82.7% .119 306.383); --color-purple-400: oklch(71.4% .203 305.504); --color-purple-500: oklch(62.7% .265 303.9); --color-purple-600: oklch(55.8% .288 302.321); --color-purple-700: oklch(49.6% .265 301.924); --color-purple-800: oklch(43.8% .218 303.724); --color-purple-900: oklch(38.1% .176 304.987); --color-purple-950: oklch(29.1% .149 302.717); --color-fuchsia-50: oklch(97.7% .017 320.058); --color-fuchsia-100: oklch(95.2% .037 318.852); --color-fuchsia-200: oklch(90.3% .076 319.62); --color-fuchsia-300: oklch(83.3% .145 321.434); --color-fuchsia-400: oklch(74% .238 322.16); --color-fuchsia-500: oklch(66.7% .295 322.15); --color-fuchsia-600: oklch(59.1% .293 322.896); --color-fuchsia-700: oklch(51.8% .253 323.949); --color-fuchsia-800: oklch(45.2% .211 324.591); --color-fuchsia-900: oklch(40.1% .17 325.612); --color-fuchsia-950: oklch(29.3% .136 325.661); --color-pink-50: oklch(97.1% .014 343.198); --color-pink-100: oklch(94.8% .028 342.258); --color-pink-200: oklch(89.9% .061 343.231); --color-pink-300: oklch(82.3% .12 346.018); --color-pink-400: oklch(71.8% .202 349.761); --color-pink-500: oklch(65.6% .241 354.308); --color-pink-600: oklch(59.2% .249 .584); --color-pink-700: oklch(52.5% .223 3.958); --color-pink-800: oklch(45.9% .187 3.815); --color-pink-900: oklch(40.8% .153 2.432); --color-pink-950: oklch(28.4% .109 3.907); --color-rose-50: oklch(96.9% .015 12.422); --color-rose-100: oklch(94.1% .03 12.58); --color-rose-200: oklch(89.2% .058 10.001); --color-rose-300: oklch(81% .117 11.638); --color-rose-400: oklch(71.2% .194 13.428); --color-rose-500: oklch(64.5% .246 16.439); --color-rose-600: oklch(58.6% .253 17.585); --color-rose-700: oklch(51.4% .222 16.935); --color-rose-800: oklch(45.5% .188 13.697); --color-rose-900: oklch(41% .159 10.272); --color-rose-950: oklch(27.1% .105 12.094); --color-slate-50: oklch(98.4% .003 247.858); --color-slate-100: oklch(96.8% .007 247.896); --color-slate-200: oklch(92.9% .013 255.508); --color-slate-300: oklch(86.9% .022 252.894); --color-slate-400: oklch(70.4% .04 256.788); --color-slate-500: oklch(55.4% .046 257.417); --color-slate-600: oklch(44.6% .043 257.281); --color-slate-700: oklch(37.2% .044 257.287); --color-slate-800: oklch(27.9% .041 260.031); --color-slate-900: oklch(20.8% .042 265.755); --color-slate-950: oklch(12.9% .042 264.695); --color-gray-50: oklch(98.5% .002 247.839); --color-gray-100: oklch(96.7% .003 264.542); --color-gray-200: oklch(92.8% .006 264.531); --color-gray-300: oklch(87.2% .01 258.338); --color-gray-400: oklch(70.7% .022 261.325); --color-gray-500: oklch(55.1% .027 264.364); --color-gray-600: oklch(44.6% .03 256.802); --color-gray-700: oklch(37.3% .034 259.733); --color-gray-800: oklch(27.8% .033 256.848); --color-gray-900: oklch(21% .034 264.665); --color-gray-950: oklch(13% .028 261.692); --color-zinc-50: oklch(98.5% 0 none); --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-800: oklch(27.4% .006 286.033); --color-zinc-900: oklch(21% .006 285.885); --color-zinc-950: oklch(14.1% .005 285.823); --color-neutral-50: oklch(98.5% 0 none); --color-neutral-100: oklch(97% 0 none); --color-neutral-200: oklch(92.2% 0 none); --color-neutral-300: oklch(87% 0 none); --color-neutral-400: oklch(70.8% 0 none); --color-neutral-500: oklch(55.6% 0 none); --color-neutral-600: oklch(43.9% 0 none); --color-neutral-700: oklch(37.1% 0 none); --color-neutral-800: oklch(26.9% 0 none); --color-neutral-900: oklch(20.5% 0 none); --color-neutral-950: oklch(14.5% 0 none); --color-stone-50: oklch(98.5% .001 106.423); --color-stone-100: oklch(97% .001 106.424); --color-stone-200: oklch(92.3% .003 48.717); --color-stone-300: oklch(86.9% .005 56.366); --color-stone-400: oklch(70.9% .01 56.259); --color-stone-500: oklch(55.3% .013 58.071); --color-stone-600: oklch(44.4% .011 73.639); --color-stone-700: oklch(37.4% .01 67.558); --color-stone-800: oklch(26.8% .007 34.298); --color-stone-900: oklch(21.6% .006 56.043); --color-stone-950: oklch(14.7% .004 49.25); --color-mauve-50: oklch(98.5% 0 none); --color-mauve-100: oklch(96% .003 325.6); --color-mauve-200: oklch(92.2% .005 325.62); --color-mauve-300: oklch(86.5% .012 325.68); --color-mauve-400: oklch(71.1% .019 323.02); --color-mauve-500: oklch(54.2% .034 322.5); --color-mauve-600: oklch(43.5% .029 321.78); --color-mauve-700: oklch(36.4% .029 323.89); --color-mauve-800: oklch(26.3% .024 320.12); --color-mauve-900: oklch(21.2% .019 322.12); --color-mauve-950: oklch(14.5% .008 326); --color-olive-50: oklch(98.8% .003 106.5); --color-olive-100: oklch(96.6% .005 106.5); --color-olive-200: oklch(93% .007 106.5); --color-olive-300: oklch(88% .011 106.6); --color-olive-400: oklch(73.7% .021 106.9); --color-olive-500: oklch(58% .031 107.3); --color-olive-600: oklch(46.6% .025 107.3); --color-olive-700: oklch(39.4% .023 107.4); --color-olive-800: oklch(28.6% .016 107.4); --color-olive-900: oklch(22.8% .013 107.4); --color-olive-950: oklch(15.3% .006 107.1); --color-mist-50: oklch(98.7% .002 197.1); --color-mist-100: oklch(96.3% .002 197.1); --color-mist-200: oklch(92.5% .005 214.3); --color-mist-300: oklch(87.2% .007 219.6); --color-mist-400: oklch(72.3% .014 214.4); --color-mist-500: oklch(56% .021 213.5); --color-mist-600: oklch(45% .017 213.2); --color-mist-700: oklch(37.8% .015 216); --color-mist-800: oklch(27.5% .011 216.9); --color-mist-900: oklch(21.8% .008 223.9); --color-mist-950: oklch(14.8% .004 228.8); --color-taupe-50: oklch(98.6% .002 67.8); --color-taupe-100: oklch(96% .002 17.2); --color-taupe-200: oklch(92.2% .005 34.3); --color-taupe-300: oklch(86.8% .007 39.5); --color-taupe-400: oklch(71.4% .014 41.2); --color-taupe-500: oklch(54.7% .021 43.1); --color-taupe-600: oklch(43.8% .017 39.3); --color-taupe-700: oklch(36.7% .016 35.7); --color-taupe-800: oklch(26.8% .011 36.5); --color-taupe-900: oklch(21.4% .009 43.1); --color-taupe-950: oklch(14.7% .004 49.3); --color-black: #000; --color-white: #fff; --spacing: .25rem; --breakpoint-sm: 40rem; --breakpoint-md: 48rem; --breakpoint-lg: 64rem; --breakpoint-xl: 80rem; --breakpoint-2xl: 96rem; --container-3xs: 16rem; --container-2xs: 18rem; --container-xs: 20rem; --container-sm: 24rem; --container-md: 28rem; --container-lg: 32rem; --container-xl: 36rem; --container-2xl: 42rem; --container-3xl: 48rem; --container-4xl: 56rem; --container-5xl: 64rem; --container-6xl: 72rem; --container-7xl: 80rem; --text-xs: .75rem; --text-xs--line-height: calc(1/.75); --text-sm: .875rem; --text-sm--line-height: calc(1.25/.875); --text-base: 1rem; --text-base--line-height: calc(1.5/1); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75/1.125); --text-xl: 1.25rem; --text-xl--line-height: calc(1.75/1.25); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2/1.5); --text-3xl: 1.875rem; --text-3xl--line-height: calc(2.25/1.875); --text-4xl: 2.25rem; --text-4xl--line-height: calc(2.5/2.25); --text-5xl: 3rem; --text-5xl--line-height: 1; --text-6xl: 3.75rem; --text-6xl--line-height: 1; --text-7xl: 4.5rem; --text-7xl--line-height: 1; --text-8xl: 6rem; --text-8xl--line-height: 1; --text-9xl: 8rem; --text-9xl--line-height: 1; --font-weight-thin: 100; --font-weight-extralight: 200; --font-weight-light: 300; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --font-weight-extrabold: 800; --font-weight-black: 900; --tracking-tighter: -.05em; --tracking-tight: -.025em; --tracking-normal: 0em; --tracking-wide: .025em; --tracking-wider: .05em; --tracking-widest: .1em; --leading-tight: 1.25; --leading-snug: 1.375; --leading-normal: 1.5; --leading-relaxed: 1.625; --leading-loose: 2; --radius-xs: .125rem; --radius-sm: .25rem; --radius-md: .375rem; --radius-lg: .5rem; --radius-xl: .75rem; --radius-2xl: 1rem; --radius-3xl: 1.5rem; --radius-4xl: 2rem; --shadow-2xs: 0 1px #0000000d; --shadow-xs: 0 1px 2px 0 #0000000d; --shadow-sm: 0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a; --shadow-md: 0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a; --shadow-lg: 0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a; --shadow-xl: 0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a; --shadow-2xl: 0 25px 50px -12px #00000040; --inset-shadow-2xs: inset 0 1px #0000000d; --inset-shadow-xs: inset 0 1px 1px #0000000d; --inset-shadow-sm: inset 0 2px 4px #0000000d; --drop-shadow-xs: 0 1px 1px #0000000d; --drop-shadow-sm: 0 1px 2px #00000026; --drop-shadow-md: 0 3px 3px #0000001f; --drop-shadow-lg: 0 4px 4px #00000026; --drop-shadow-xl: 0 9px 7px #0000001a; --drop-shadow-2xl: 0 25px 25px #00000026; --text-shadow-2xs: 0px 1px 0px #00000026; --text-shadow-xs: 0px 1px 1px #0003; --text-shadow-sm: 0px 1px 0px #00000013,0px 1px 1px #00000013,0px 2px 2px #00000013; --text-shadow-md: 0px 1px 1px #0000001a,0px 1px 2px #0000001a,0px 2px 4px #0000001a; --text-shadow-lg: 0px 1px 2px #0000001a,0px 3px 2px #0000001a,0px 4px 8px #0000001a; --ease-in: cubic-bezier(.4,0,1,1); --ease-out: cubic-bezier(0,0,.2,1); --ease-in-out: cubic-bezier(.4,0,.2,1); --animate-spin: spin 1s linear infinite; --animate-ping: ping 1s cubic-bezier(0,0,.2,1)infinite; --animate-pulse: pulse 2s cubic-bezier(.4,0,.6,1)infinite; --animate-bounce: bounce 1s infinite; @keyframes spin { to { transform: rotate(360deg); }}@keyframes ping { 75%,100% { transform: scale(2); opacity: 0; }}@keyframes pulse { 50% { opacity: .5; }}@keyframes bounce { 0%,100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(.8,0,1,1); }50% { transform: none; animation-timing-function: cubic-bezier(0,0,.2,1); }}--blur-xs: 4px; --blur-sm: 8px; --blur-md: 12px; --blur-lg: 16px; --blur-xl: 24px; --blur-2xl: 40px; --blur-3xl: 64px; --perspective-dramatic: 100px; --perspective-near: 300px; --perspective-normal: 500px; --perspective-midrange: 800px; --perspective-distant: 1200px; --aspect-video: 16/9; --default-transition-duration: .15s; --default-transition-timing-function: cubic-bezier(.4,0,.2,1); --default-font-family: --theme(--font-sans,initial); --default-font-feature-settings: --theme(--font-sans--font-feature-settings,initial); --default-font-variation-settings: --theme(--font-sans--font-variation-settings,initial); --default-mono-font-family: --theme(--font-mono,initial); --default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings,initial); --default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings,initial);}@theme default inline reference{--blur: 8px; --shadow: 0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a; --shadow-inner: inset 0 2px 4px 0 #0000000d; --drop-shadow: 0 1px 2px #0000001a,0 1px 1px #0000000f; --radius: .25rem; --max-width-prose: 65ch;}}@layer base{*,:after,:before{box-sizing:border-box;border:0 solid;margin:0;padding:0}::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::-webkit-file-upload-button{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:--theme(--default-font-family,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:--theme(--default-font-feature-settings,normal);font-variation-settings:--theme(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{color:inherit;border-top-width:1px;height:0}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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:--theme(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:--theme(--default-mono-font-feature-settings,normal);font-variation-settings:--theme(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{position:relative;vertical-align:baseline;font-size:75%;line-height:0}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}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}::-webkit-file-upload-button{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:-webkit-any([multiple],[size])) optgroup{font-weight:bolder}:where(select:-moz-any([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:-webkit-any([multiple],[size])) optgroup option{padding-inline-start:20px}:where(select:-moz-any([multiple],[size])) optgroup option{padding-inline-start:20px}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::-webkit-file-upload-button{margin-inline-end:4px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports ( not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{text-align:inherit;min-height:1lh}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-year-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-month-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-day-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-hour-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-minute-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-second-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-millisecond-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-meridiem-field{padding-block-start:0;padding-block-end:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button{appearance:button}input:where([type=button],[type=reset],[type=submit]){appearance:button}::-webkit-file-upload-button{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 utilities{@tailwind utilities;}:root{--buncss-light: ;--buncss-dark:initial;color-scheme:dark;--page:oklch(.16 .02 307);--surface:oklch(.19 .022 307);--surface-2:oklch(.235 .026 307);--border:oklch(1 0 0/9%);--border-strong:oklch(1 0 0/13%);--text:oklch(.92 .01 307);--muted:oklch(.71 .016 307);--dim:oklch(.53 .018 307);--accent:oklch(.674 .23 341);--accent-fg:oklch(.2 .07 341);--accent-muted:oklch(.674 .23 341/18%);--link:oklch(.817 .112 205);--success:oklch(.76 .14 150);--danger:oklch(.66 .19 25);--warning:oklch(.79 .14 85);--text-primary:var(--text);--text-secondary:var(--muted);--text-muted:var(--dim);--border-subtle:var(--border);--accent-red:var(--danger);--accent-green:var(--success);--accent-cyan:var(--link);--bg-hover:var(--surface-2);--bg-elevated:var(--surface-2);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--radius-sm:3px;--radius-md:6px;--radius-lg:8px}@media (prefers-color-scheme:light){:root:not([data-theme]){--buncss-light:initial;--buncss-dark: ;color-scheme:light;--page:oklch(.985 .004 307);--surface:oklch(1 0 0);--surface-2:oklch(.965 .006 307);--border:oklch(0 0 0/10%);--border-strong:oklch(0 0 0/15%);--text:oklch(.26 .03 307);--muted:oklch(.46 .03 307);--dim:oklch(.58 .025 307);--accent:oklch(.62 .23 341);--accent-fg:oklch(.99 .01 341);--accent-muted:oklch(.62 .23 341/14%);--link:oklch(.58 .13 230);--success:oklch(.55 .13 150);--danger:oklch(.55 .19 25);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--warning:oklch(.6 .13 85)}}[data-theme=light]{--buncss-light:initial;--buncss-dark: ;color-scheme:light;--page:oklch(.985 .004 307);--surface:oklch(1 0 0);--surface-2:oklch(.965 .006 307);--border:oklch(0 0 0/10%);--border-strong:oklch(0 0 0/15%);--text:oklch(.26 .03 307);--muted:oklch(.46 .03 307);--dim:oklch(.58 .025 307);--accent:oklch(.62 .23 341);--accent-fg:oklch(.99 .01 341);--accent-muted:oklch(.62 .23 341/14%);--link:oklch(.58 .13 230);--success:oklch(.55 .13 150);--danger:oklch(.55 .19 25);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--warning:oklch(.6 .13 85)}@layer base{*{box-sizing:border-box}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{background:var(--page);color:var(--text);min-height:100vh;margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}button:focus-visible{outline:2px solid var(--link);outline-offset:2px}a:focus-visible{outline:2px solid var(--link);outline-offset:2px}[tabindex]:focus-visible{outline:2px solid var(--link);outline-offset:2px}}.stats-app-container{display:flex;background:var(--page);color:var(--text);min-height:100vh}.stats-nav-rail{background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;width:240px}.stats-desktop-nav{display:flex}@media (max-width:1023px){.stats-desktop-nav{display:none}}.stats-nav-rail-header{border-bottom:1px solid var(--border);display:flex;align-items: center;height:56px;padding:20px 24px}.stats-logo-container{display:flex;flex-direction:column}.stats-logo-text{letter-spacing:.1em;color:var(--text);font-size:14px;font-weight:700}.stats-logo-subtext{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-size:10px}.stats-nav-rail-menu{display:flex;flex-direction:column;flex:1;gap:4px;padding:16px 12px}.stats-nav-rail-item{display:flex;color:var(--muted);border-radius:var(--radius-md);cursor:pointer;text-align:left;background:0 0;border:none;align-items: center;gap:12px;padding:10px 16px;transition:all .15s;font-size:13px;font-weight:500}.stats-nav-rail-item:hover{color:var(--text);background:var(--surface-2)}.stats-nav-rail-item[data-active=true]{color:var(--accent);background:var(--accent-muted);position:relative}.stats-nav-rail-item[data-active=true]:before{content:"";position:absolute;background:var(--accent);border-radius:0 3px 3px 0;width:3px;height:18px;top:50%;left:0;transform:translateY(-50%)}.stats-nav-rail-item-icon{flex-shrink:0}.stats-nav-rail-footer{border-top:1px solid var(--border);padding:16px 24px}.stats-version-tag{color:var(--dim);font-size:11px}.stats-main-pane{display:flex;flex-direction:column;flex:1;min-width:0}.stats-top-bar{background:var(--surface);border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items: center;height:56px;padding:0 24px}@media (max-width:767px){.stats-top-bar{flex-wrap:wrap;gap:12px;height:auto;min-height:56px;padding:8px 16px}}.stats-top-bar-left{display:flex;align-items: center;gap:12px}.stats-mobile-menu-btn{display:none;color:var(--text);cursor:pointer;background:0 0;border:none;padding:4px}@media (max-width:1023px){.stats-mobile-menu-btn{display:inline-flex;justify-content:center;align-items: center}}.stats-page-title{color:var(--text);margin:0;font-size:16px;font-weight:600}.stats-top-bar-right{display:flex;align-items: center;gap:16px}@media (max-width:767px){.stats-top-bar-right{justify-content:space-between;gap:8px;width:100%}}.stats-top-bar-meta{display:flex;align-items: center}.stats-last-updated{color:var(--dim);font-variant-numeric:tabular-nums;font-size:11px}.stats-content-area{overflow-y:auto;flex:1}.stats-content-inner{display:flex;flex-direction:column;gap:24px;max-width:1560px;margin:0 auto;padding:24px}@media (max-width:767px){.stats-content-inner{gap:16px;padding:16px}}.stats-mobile-drawer-overlay{position:fixed;backdrop-filter:none;z-index:100;background:#0006;inset:0}.stats-mobile-drawer{position:absolute;background:var(--surface);border-right:1px solid var(--border);display:flex;z-index:101;animation:.2s ease-out slideIn;flex-direction:column;width:260px;top:0;bottom:0;left:0}@keyframes slideIn{0%{transform:translate(-100%)}to{transform:translate(0)}}.stats-mobile-drawer-header{display:flex;border-bottom:1px solid var(--border);justify-content:space-between;align-items: center;height:56px;padding:0 16px 0 24px}.stats-mobile-nav{display:flex;height:calc(100% - 56px)}.stats-button{display:inline-flex;border-radius:var(--radius-md);cursor:pointer;border:1px solid #0000;justify-content:center;align-items: center;gap:8px;padding:8px 14px;transition:all .15s;font-family:inherit;font-size:13px;font-weight:500}.stats-button:active:not(:disabled){transform:translateY(1px)}.stats-button-primary{background:var(--accent);color:var(--accent-fg)}.stats-button-primary:hover:not(:disabled){opacity:.9}.stats-button-primary:disabled{opacity:.5;cursor:not-allowed}.stats-button-secondary{background:var(--surface-2);color:var(--text);border-color:var(--border)}.stats-button-secondary:hover:not(:disabled){background:var(--border)}.stats-button-secondary:disabled{opacity:.5;cursor:not-allowed}.stats-range-control{display:flex;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:2px}.stats-range-control-btn{color:var(--muted);border-radius:var(--radius-sm);cursor:pointer;background:0 0;border:none;padding:4px 10px;transition:all .1s;font-size:12px;font-weight:500}.stats-range-control-btn:hover{color:var(--text)}.stats-range-control-btn[data-active=true]{color:var(--text);background:var(--surface);box-shadow:0 1px 2px #0000002e}.stats-select{appearance:none;color:var(--text);background-color:var(--surface-2);background-image:var(--select-chevron);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;text-overflow:ellipsis;background-position:right 10px center;background-repeat:no-repeat;max-width:100%;padding:6px 28px 6px 12px;transition:border-color .15s,background-color .15s;font-family:inherit;font-size:13px;font-weight:500}.stats-select:hover:not(:disabled){border-color:var(--border-strong);background-color:var(--bg-hover)}.stats-select:focus-visible{outline:2px solid var(--link);outline-offset:1px}.stats-select:disabled{opacity:.5;cursor:not-allowed}@media (pointer:coarse){.stats-select{font-size:16px}}.stats-select option{background-color:var(--surface);color:var(--text)}@media (forced-colors:active){.stats-select{appearance:auto}}.stats-sync-container{display:flex;align-items: center;gap:12px}.stats-sync-status-msg{font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;font-size:11px}.stats-sync-status-msg[data-type=success]{color:var(--success)}.stats-sync-status-msg[data-type=error]{color:var(--danger)}.stats-sync-icon{flex-shrink:0}.stats-spin{animation:1s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.stats-panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column}.stats-panel-header{border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items: center;gap:16px;padding:16px 20px}@media (max-width:480px){.stats-panel-header{flex-direction:column;align-items: flex-start;gap:8px}}.stats-panel-header-titles{display:flex;flex-direction:column}.stats-panel-title{color:var(--text);margin:0;font-size:14px;font-weight:600}.stats-panel-subtitle{color:var(--muted);margin:2px 0 0;font-size:12px}.stats-panel-actions{display:flex;align-items: center;gap:12px}.stats-panel-body{padding:20px}.stats-metric-cluster{display:flex;flex-direction:column;gap:16px}.stats-metric-primary-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:16px}@media (max-width:1023px){.stats-metric-primary-grid{grid-template-columns:repeat(2,1fr)}}@media (max-width:480px){.stats-metric-primary-grid{grid-template-columns:1fr}}.stats-metric-secondary-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:12px}@media (max-width:1279px){.stats-metric-secondary-grid{grid-template-columns:repeat(4,1fr)}}@media (max-width:767px){.stats-metric-secondary-grid{grid-template-columns:repeat(2,1fr)}}.stats-metric-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;justify-content:space-between;padding:16px}.stats-metric-card.primary{padding:20px 18px}.stats-metric-card.primary .stats-metric-label{color:var(--accent)}.stats-metric-card.secondary{padding:12px 14px}.stats-metric-label{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:8px;font-size:11px;font-weight:600}.stats-metric-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:20px;font-weight:700}.stats-metric-card.secondary .stats-metric-value{font-size:15px;font-weight:600}.stats-table-wrapper{width:100%}.stats-table-container{overflow-x:auto;width:100%}.stats-table{border-collapse:collapse;font-variant-numeric:tabular-nums;width:100%;font-size:13px}.stats-table-th{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);border-bottom:2px solid var(--border);padding:12px 16px;font-size:11px;font-weight:600}.stats-table-td{border-bottom:1px solid var(--border);color:var(--text);padding:10px 16px}.stats-table-tr{background:0 0}.stats-table-tr-clickable{cursor:pointer;transition:background .12s,box-shadow .12s}.stats-table-tr-clickable:hover{background:var(--surface-2);box-shadow:inset 2px 0 0 var(--accent)}.stats-text-left{text-align:left}.stats-text-right{text-align:right}.stats-table-empty{text-align:center;color:var(--muted);border:1px dashed var(--border);border-radius:var(--radius-lg);padding:40px;font-size:13px}.stats-progress-bar-track{background:var(--surface-2);overflow:hidden;border-radius:999px}.stats-progress-bar-fill{border-radius:inherit;height:100%;transition:width .18s}.stats-progress-bar-fill[data-variant=link]{background:var(--link)}.stats-progress-bar-fill[data-variant=success]{background:var(--success)}.stats-table-mobile-only{display:none}.stats-table-desktop-only{overflow-x:auto;width:100%}@media (max-width:767px){.stats-table-desktop-only{display:none}.stats-table-mobile-only{display:block}}.stats-table-mobile-list{display:flex;flex-direction:column;gap:12px}.stats-table-mobile-card-wrapper{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:14px}.stats-status-pill{display:inline-flex;border-radius:100px;justify-content:center;align-items: center;padding:2px 8px;font-size:11px;font-weight:600;line-height:1.2}.stats-status-pill[data-variant=success]{background:color-mix(in srgb,var(--success)18%,transparent);color:var(--success)}.stats-status-pill[data-variant=danger]{background:color-mix(in srgb,var(--danger)18%,transparent);color:var(--danger)}.stats-status-pill[data-variant=warning]{background:color-mix(in srgb,var(--warning)18%,transparent);color:var(--warning)}.stats-status-pill[data-variant=info]{background:color-mix(in srgb,var(--link)18%,transparent);color:var(--link)}.stats-status-pill[data-variant=default]{background:var(--surface-2);color:var(--muted)}.stats-segmented-control{display:flex;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-md);gap:2px;padding:2px}.stats-segmented-control-btn{color:var(--muted);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:6px 12px;transition:all .1s;font-size:12px;font-weight:500}.stats-segmented-control-btn:hover{color:var(--text)}.stats-segmented-control-btn[data-active=true]{color:var(--text);background:var(--surface);box-shadow:0 1px 2px #0000002e}.stats-drawer-overlay{position:fixed;z-index:100;display:flex;background:#00000080;justify-content:flex-end;inset:0}.stats-drawer{background:var(--surface);border-left:1px solid var(--border);display:flex;animation:.25s cubic-bezier(.16,1,.3,1) drawerSlide;flex-direction:column;width:600px;max-width:100%;height:100%;box-shadow:-4px 0 24px #00000040}@keyframes drawerSlide{0%{transform:translate(100%)}to{transform:translate(0)}}.stats-drawer-header{border-bottom:1px solid var(--border);display:flex;background:var(--surface);flex-shrink:0;justify-content:space-between;align-items: center;height:56px;padding:0 24px}.stats-drawer-header-left{display:flex;align-items:baseline;gap:12px}.stats-drawer-title{color:var(--text);margin:0;font-size:15px;font-weight:600}.stats-drawer-id{color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px}.stats-drawer-close-btn{color:var(--muted);cursor:pointer;display:inline-flex;border-radius:var(--radius-sm);background:0 0;border:none;justify-content:center;align-items: center;padding:4px;transition:all .1s}.stats-drawer-close-btn:hover{color:var(--text);background:var(--surface-2)}.stats-drawer-body{overflow-y:auto;flex:1;padding:24px}.stats-drawer-content{display:flex;flex-direction:column;gap:24px}.stats-drawer-status-card{background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px}.stats-drawer-status-row{display:flex;justify-content:space-between;align-items: flex-start;gap:16px}.stats-drawer-model{color:var(--text);font-size:16px;font-weight:700}.stats-drawer-provider{color:var(--muted);margin-top:2px;font-size:12px}.stats-drawer-error-block{border-top:1px solid var(--border);margin-top:14px;padding-top:12px}.stats-drawer-error-label{text-transform:uppercase;letter-spacing:.05em;color:var(--danger);margin-bottom:4px;font-size:11px;font-weight:600}.stats-drawer-error-text{color:var(--text);white-space:pre-wrap;word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px}.stats-drawer-metrics-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}.stats-drawer-metric-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;justify-content:center;padding:14px}.stats-drawer-metric-label{display:flex;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);align-items: center;gap:6px;margin-bottom:6px;font-size:11px;font-weight:600}.stats-drawer-metric-icon{color:var(--dim)}.stats-drawer-metric-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:16px;font-weight:700}.stats-drawer-metric-sub{color:var(--muted);font-variant-numeric:tabular-nums;margin-top:2px;font-size:11px}.stats-drawer-json-blocks{display:flex;flex-direction:column;gap:16px}.stats-json-block{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface);overflow:hidden}.stats-json-block-header{background:var(--surface-2);border-bottom:1px solid var(--border);display:flex;cursor:pointer;user-select:none;justify-content:space-between;align-items: center;padding:10px 16px;font-size:12px;font-weight:600}.stats-json-block-header:hover{background:var(--border)}.stats-json-block-title{color:var(--text)}.stats-json-block-toggle-indicator{color:var(--muted)}.stats-json-block-content-wrapper{background:var(--page);overflow-x:auto;padding:16px}.stats-json-block-content{color:var(--text);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px;line-height:1.5}.stats-skeleton{background:var(--surface-2);position:relative;overflow:hidden;border-radius:var(--radius-sm)}.stats-skeleton[data-variant=text]{height:14px;margin-top:4px;margin-bottom:4px}.stats-skeleton[data-variant=circle]{border-radius:50%}.stats-skeleton:after{content:"";position:absolute;animation:1.5s infinite shimmer;background:linear-gradient(90deg,#0000 0%,#ffffff0d 50%,#0000 100%);inset:0;transform:translate(-100%)}@media (prefers-color-scheme:light){.stats-skeleton:after{background:linear-gradient(90deg,#0000 0%,#00000008 50%,#0000 100%)}}@keyframes shimmer{to{transform:translate(100%)}}@media (prefers-reduced-motion:reduce){.stats-skeleton:after{animation:none}}.stats-boundary-skeleton{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface);padding:20px}.stats-empty-state,.stats-error-state{display:flex;text-align:center;background:var(--surface);border:1px dashed var(--border);border-radius:var(--radius-lg);flex-direction:column;justify-content:center;align-items: center;padding:48px 24px}.stats-empty-state-message{color:var(--muted);margin:0;font-size:13px}.stats-error-state-content{max-width:360px}.stats-error-state-title{color:var(--text);margin:0 0 8px;font-size:14px;font-weight:600}.stats-error-state-message{color:var(--danger);word-break:break-word;margin:0 0 16px;font-size:12px}.stats-error-state-btn{margin:0 auto}.stats-text-primary{color:var(--text)}.stats-text-secondary{color:var(--muted)}.stats-text-muted{color:var(--dim)}.stats-text-warning{color:var(--warning)}.stats-text-xs{font-size:12px}.stats-font-medium{font-weight:500}.stats-font-semibold{font-weight:600}*{scrollbar-width:thin;scrollbar-color:var(--border-strong)transparent}*::-webkit-scrollbar{width:10px;height:10px}*::-webkit-scrollbar-track{background:0 0}*::-webkit-scrollbar-thumb{background:var(--border-strong);background-clip:padding-box;border:2px solid #0000;border-radius:999px}*::-webkit-scrollbar-thumb:hover{background:var(--dim);background-clip:padding-box;border:2px solid #0000}*::-webkit-scrollbar-corner{background:0 0}@media (prefers-reduced-motion:reduce){.stats-button,.stats-table-tr-clickable,.stats-nav-rail-item,.stats-range-control-btn,.stats-segmented-control-btn{transition:none}.stats-button:active:not(:disabled){transform:none}.stats-mobile-drawer,.stats-drawer{animation:none}}.stats-logo-container{display:grid;grid-template-columns:auto 1fr;align-items: center;column-gap:10px}.stats-logo-container:before{content:"";grid-row:1/3;background:linear-gradient(135deg,#ed4abf 0%,#9b4dff 50%,#5ad8e6 100%);border-radius:4px;width:16px;height:16px}.stats-logo-text,.stats-logo-subtext{grid-column:2}.stats-json-actions{display:flex;align-items: center;gap:12px}.stats-json-copy-btn{display:inline-flex;color:var(--muted);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;background:0 0;align-items: center;gap:5px;padding:3px 8px;transition:color .12s,background .12s;font-family:inherit;font-size:11px;font-weight:600}.stats-json-copy-btn:hover{color:var(--text);background:var(--surface-2)}.stats-empty-state-icon{color:var(--dim);margin-bottom:12px}@media (prefers-reduced-motion:reduce){.stats-json-copy-btn{transition:none}}.stats-theme-toggle{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface);color:var(--muted);cursor:pointer;flex-shrink:0;justify-content:center;align-items: center;width:32px;height:32px;transition:color .14s,background .14s,border-color .14s}.stats-theme-toggle:hover{color:var(--text);border-color:var(--border-strong)}@media (prefers-reduced-motion:reduce){.stats-theme-toggle{transition:none}}.stats-trace-view{display:flex;flex-direction:column;gap:14px}.stats-trace-header{display:flex;flex-wrap:wrap;align-items: center;gap:12px;min-width:0}.stats-trace-back{display:inline-flex;color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;align-items: center;gap:6px;padding:6px 12px;transition:color .14s,border-color .14s;font-size:12px}.stats-trace-back:hover{color:var(--text);border-color:var(--border-strong)}.stats-trace-back:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-title{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;margin:0;font-size:15px;font-weight:600}.stats-trace-cwd{color:var(--dim);border:1px solid var(--border);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:999px;max-width:280px;padding:3px 10px;font-size:11px}.stats-trace-summary{display:flex;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;flex-wrap:wrap}.stats-trace-summary-cell{display:flex;border-right:1px solid var(--border);flex-direction:column;gap:3px;min-width:76px;padding:10px 16px}.stats-trace-summary-cell:last-child{border-right:none}.stats-trace-summary-label{text-transform:uppercase;letter-spacing:.07em;color:var(--dim);font-size:9px}.stats-trace-summary-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:13px;font-weight:500}.stats-trace-toolbar{display:flex;flex-wrap:wrap;align-items: center;gap:12px}.stats-trace-toolbar-hint{color:var(--dim);margin-left:auto;font-size:10px}.stats-trace-input{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text);padding:6px 10px;font-size:12px}.stats-trace-input:focus-visible{outline:2px solid var(--link);outline-offset:1px}.stats-trace-input::placeholder{color:var(--dim)}.stats-trace-check{display:inline-flex;color:var(--muted);cursor:pointer;user-select:none;align-items: center;gap:6px;font-size:12px}.stats-trace-icon-btn{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-md);color:var(--muted);cursor:pointer;background:0 0;justify-content:center;align-items: center;width:26px;height:26px;transition:color .14s,border-color .14s}.stats-trace-icon-btn:hover:not(:disabled){color:var(--text);border-color:var(--border-strong)}.stats-trace-icon-btn:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-icon-btn:disabled{opacity:.4;cursor:default}.stats-trace-timeline-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;gap:10px;padding:12px}.stats-trace-timeline{position:relative;display:flex;overflow-y:auto;overflow-x:hidden;max-height:560px}.stats-trace-canvas{cursor:grab;outline:none;touch-action:none;border-radius:var(--radius-sm)}.stats-trace-canvas:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-canvas:active{cursor:grabbing}.stats-trace-chevron{display:inline-flex;cursor:pointer;color:var(--muted);background:0 0;border:none;padding:0}.stats-trace-chevron:hover{color:var(--text)}.stats-trace-gutter-track{color:var(--text);font-size:11px;font-weight:500}.stats-trace-gutter-model{color:var(--dim);font-size:9px}.stats-trace-gutter-lane{position:absolute;color:var(--dim);text-transform:uppercase;letter-spacing:.08em;font-size:8px}.stats-trace-list{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);overflow-y:auto;height:340px}.stats-trace-row{display:flex;border:none;border-bottom:1px solid var(--border);text-align:left;cursor:pointer;background:0 0;align-items: center;gap:10px;width:100%;height:28px;padding:0 12px;transition:background .1s}.stats-trace-row:hover{background:var(--bg-hover)}.stats-trace-row:focus-visible{outline:2px solid var(--link);outline-offset:-2px}.stats-trace-row[data-selected=true]{background:var(--accent-muted)}.stats-trace-row-static{cursor:default}.stats-trace-row-static:hover{background:0 0}.stats-trace-kind{text-transform:uppercase;letter-spacing:.06em;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:3px;flex-shrink:0;width:58px;padding:2px 0;font-size:8px}.stats-trace-row-meta{color:var(--dim);font-variant-numeric:tabular-nums;display:flex;flex-shrink:0;gap:10px;margin-left:auto;font-size:10px}@media (prefers-reduced-motion:reduce){.stats-trace-back,.stats-trace-icon-btn,.stats-trace-row{transition:none}}
1
+ @layer theme,base,components,utilities;@layer theme{@theme default{--font-sans: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; --font-serif: ui-serif,Georgia,Cambria,"Times New Roman",Times,serif; --font-mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; --color-red-50: oklch(97.1% .013 17.38); --color-red-100: oklch(93.6% .032 17.717); --color-red-200: oklch(88.5% .062 18.334); --color-red-300: oklch(80.8% .114 19.571); --color-red-400: oklch(70.4% .191 22.216); --color-red-500: oklch(63.7% .237 25.331); --color-red-600: oklch(57.7% .245 27.325); --color-red-700: oklch(50.5% .213 27.518); --color-red-800: oklch(44.4% .177 26.899); --color-red-900: oklch(39.6% .141 25.723); --color-red-950: oklch(25.8% .092 26.042); --color-orange-50: oklch(98% .016 73.684); --color-orange-100: oklch(95.4% .038 75.164); --color-orange-200: oklch(90.1% .076 70.697); --color-orange-300: oklch(83.7% .128 66.29); --color-orange-400: oklch(75% .183 55.934); --color-orange-500: oklch(70.5% .213 47.604); --color-orange-600: oklch(64.6% .222 41.116); --color-orange-700: oklch(55.3% .195 38.402); --color-orange-800: oklch(47% .157 37.304); --color-orange-900: oklch(40.8% .123 38.172); --color-orange-950: oklch(26.6% .079 36.259); --color-amber-50: oklch(98.7% .022 95.277); --color-amber-100: oklch(96.2% .059 95.617); --color-amber-200: oklch(92.4% .12 95.746); --color-amber-300: oklch(87.9% .169 91.605); --color-amber-400: oklch(82.8% .189 84.429); --color-amber-500: oklch(76.9% .188 70.08); --color-amber-600: oklch(66.6% .179 58.318); --color-amber-700: oklch(55.5% .163 48.998); --color-amber-800: oklch(47.3% .137 46.201); --color-amber-900: oklch(41.4% .112 45.904); --color-amber-950: oklch(27.9% .077 45.635); --color-yellow-50: oklch(98.7% .026 102.212); --color-yellow-100: oklch(97.3% .071 103.193); --color-yellow-200: oklch(94.5% .129 101.54); --color-yellow-300: oklch(90.5% .182 98.111); --color-yellow-400: oklch(85.2% .199 91.936); --color-yellow-500: oklch(79.5% .184 86.047); --color-yellow-600: oklch(68.1% .162 75.834); --color-yellow-700: oklch(55.4% .135 66.442); --color-yellow-800: oklch(47.6% .114 61.907); --color-yellow-900: oklch(42.1% .095 57.708); --color-yellow-950: oklch(28.6% .066 53.813); --color-lime-50: oklch(98.6% .031 120.757); --color-lime-100: oklch(96.7% .067 122.328); --color-lime-200: oklch(93.8% .127 124.321); --color-lime-300: oklch(89.7% .196 126.665); --color-lime-400: oklch(84.1% .238 128.85); --color-lime-500: oklch(76.8% .233 130.85); --color-lime-600: oklch(64.8% .2 131.684); --color-lime-700: oklch(53.2% .157 131.589); --color-lime-800: oklch(45.3% .124 130.933); --color-lime-900: oklch(40.5% .101 131.063); --color-lime-950: oklch(27.4% .072 132.109); --color-green-50: oklch(98.2% .018 155.826); --color-green-100: oklch(96.2% .044 156.743); --color-green-200: oklch(92.5% .084 155.995); --color-green-300: oklch(87.1% .15 154.449); --color-green-400: oklch(79.2% .209 151.711); --color-green-500: oklch(72.3% .219 149.579); --color-green-600: oklch(62.7% .194 149.214); --color-green-700: oklch(52.7% .154 150.069); --color-green-800: oklch(44.8% .119 151.328); --color-green-900: oklch(39.3% .095 152.535); --color-green-950: oklch(26.6% .065 152.934); --color-emerald-50: oklch(97.9% .021 166.113); --color-emerald-100: oklch(95% .052 163.051); --color-emerald-200: oklch(90.5% .093 164.15); --color-emerald-300: oklch(84.5% .143 164.978); --color-emerald-400: oklch(76.5% .177 163.223); --color-emerald-500: oklch(69.6% .17 162.48); --color-emerald-600: oklch(59.6% .145 163.225); --color-emerald-700: oklch(50.8% .118 165.612); --color-emerald-800: oklch(43.2% .095 166.913); --color-emerald-900: oklch(37.8% .077 168.94); --color-emerald-950: oklch(26.2% .051 172.552); --color-teal-50: oklch(98.4% .014 180.72); --color-teal-100: oklch(95.3% .051 180.801); --color-teal-200: oklch(91% .096 180.426); --color-teal-300: oklch(85.5% .138 181.071); --color-teal-400: oklch(77.7% .152 181.912); --color-teal-500: oklch(70.4% .14 182.503); --color-teal-600: oklch(60% .118 184.704); --color-teal-700: oklch(51.1% .096 186.391); --color-teal-800: oklch(43.7% .078 188.216); --color-teal-900: oklch(38.6% .063 188.416); --color-teal-950: oklch(27.7% .046 192.524); --color-cyan-50: oklch(98.4% .019 200.873); --color-cyan-100: oklch(95.6% .045 203.388); --color-cyan-200: oklch(91.7% .08 205.041); --color-cyan-300: oklch(86.5% .127 207.078); --color-cyan-400: oklch(78.9% .154 211.53); --color-cyan-500: oklch(71.5% .143 215.221); --color-cyan-600: oklch(60.9% .126 221.723); --color-cyan-700: oklch(52% .105 223.128); --color-cyan-800: oklch(45% .085 224.283); --color-cyan-900: oklch(39.8% .07 227.392); --color-cyan-950: oklch(30.2% .056 229.695); --color-sky-50: oklch(97.7% .013 236.62); --color-sky-100: oklch(95.1% .026 236.824); --color-sky-200: oklch(90.1% .058 230.902); --color-sky-300: oklch(82.8% .111 230.318); --color-sky-400: oklch(74.6% .16 232.661); --color-sky-500: oklch(68.5% .169 237.323); --color-sky-600: oklch(58.8% .158 241.966); --color-sky-700: oklch(50% .134 242.749); --color-sky-800: oklch(44.3% .11 240.79); --color-sky-900: oklch(39.1% .09 240.876); --color-sky-950: oklch(29.3% .066 243.157); --color-blue-50: oklch(97% .014 254.604); --color-blue-100: oklch(93.2% .032 255.585); --color-blue-200: oklch(88.2% .059 254.128); --color-blue-300: oklch(80.9% .105 251.813); --color-blue-400: oklch(70.7% .165 254.624); --color-blue-500: oklch(62.3% .214 259.815); --color-blue-600: oklch(54.6% .245 262.881); --color-blue-700: oklch(48.8% .243 264.376); --color-blue-800: oklch(42.4% .199 265.638); --color-blue-900: oklch(37.9% .146 265.522); --color-blue-950: oklch(28.2% .091 267.935); --color-indigo-50: oklch(96.2% .018 272.314); --color-indigo-100: oklch(93% .034 272.788); --color-indigo-200: oklch(87% .065 274.039); --color-indigo-300: oklch(78.5% .115 274.713); --color-indigo-400: oklch(67.3% .182 276.935); --color-indigo-500: oklch(58.5% .233 277.117); --color-indigo-600: oklch(51.1% .262 276.966); --color-indigo-700: oklch(45.7% .24 277.023); --color-indigo-800: oklch(39.8% .195 277.366); --color-indigo-900: oklch(35.9% .144 278.697); --color-indigo-950: oklch(25.7% .09 281.288); --color-violet-50: oklch(96.9% .016 293.756); --color-violet-100: oklch(94.3% .029 294.588); --color-violet-200: oklch(89.4% .057 293.283); --color-violet-300: oklch(81.1% .111 293.571); --color-violet-400: oklch(70.2% .183 293.541); --color-violet-500: oklch(60.6% .25 292.717); --color-violet-600: oklch(54.1% .281 293.009); --color-violet-700: oklch(49.1% .27 292.581); --color-violet-800: oklch(43.2% .232 292.759); --color-violet-900: oklch(38% .189 293.745); --color-violet-950: oklch(28.3% .141 291.089); --color-purple-50: oklch(97.7% .014 308.299); --color-purple-100: oklch(94.6% .033 307.174); --color-purple-200: oklch(90.2% .063 306.703); --color-purple-300: oklch(82.7% .119 306.383); --color-purple-400: oklch(71.4% .203 305.504); --color-purple-500: oklch(62.7% .265 303.9); --color-purple-600: oklch(55.8% .288 302.321); --color-purple-700: oklch(49.6% .265 301.924); --color-purple-800: oklch(43.8% .218 303.724); --color-purple-900: oklch(38.1% .176 304.987); --color-purple-950: oklch(29.1% .149 302.717); --color-fuchsia-50: oklch(97.7% .017 320.058); --color-fuchsia-100: oklch(95.2% .037 318.852); --color-fuchsia-200: oklch(90.3% .076 319.62); --color-fuchsia-300: oklch(83.3% .145 321.434); --color-fuchsia-400: oklch(74% .238 322.16); --color-fuchsia-500: oklch(66.7% .295 322.15); --color-fuchsia-600: oklch(59.1% .293 322.896); --color-fuchsia-700: oklch(51.8% .253 323.949); --color-fuchsia-800: oklch(45.2% .211 324.591); --color-fuchsia-900: oklch(40.1% .17 325.612); --color-fuchsia-950: oklch(29.3% .136 325.661); --color-pink-50: oklch(97.1% .014 343.198); --color-pink-100: oklch(94.8% .028 342.258); --color-pink-200: oklch(89.9% .061 343.231); --color-pink-300: oklch(82.3% .12 346.018); --color-pink-400: oklch(71.8% .202 349.761); --color-pink-500: oklch(65.6% .241 354.308); --color-pink-600: oklch(59.2% .249 .584); --color-pink-700: oklch(52.5% .223 3.958); --color-pink-800: oklch(45.9% .187 3.815); --color-pink-900: oklch(40.8% .153 2.432); --color-pink-950: oklch(28.4% .109 3.907); --color-rose-50: oklch(96.9% .015 12.422); --color-rose-100: oklch(94.1% .03 12.58); --color-rose-200: oklch(89.2% .058 10.001); --color-rose-300: oklch(81% .117 11.638); --color-rose-400: oklch(71.2% .194 13.428); --color-rose-500: oklch(64.5% .246 16.439); --color-rose-600: oklch(58.6% .253 17.585); --color-rose-700: oklch(51.4% .222 16.935); --color-rose-800: oklch(45.5% .188 13.697); --color-rose-900: oklch(41% .159 10.272); --color-rose-950: oklch(27.1% .105 12.094); --color-slate-50: oklch(98.4% .003 247.858); --color-slate-100: oklch(96.8% .007 247.896); --color-slate-200: oklch(92.9% .013 255.508); --color-slate-300: oklch(86.9% .022 252.894); --color-slate-400: oklch(70.4% .04 256.788); --color-slate-500: oklch(55.4% .046 257.417); --color-slate-600: oklch(44.6% .043 257.281); --color-slate-700: oklch(37.2% .044 257.287); --color-slate-800: oklch(27.9% .041 260.031); --color-slate-900: oklch(20.8% .042 265.755); --color-slate-950: oklch(12.9% .042 264.695); --color-gray-50: oklch(98.5% .002 247.839); --color-gray-100: oklch(96.7% .003 264.542); --color-gray-200: oklch(92.8% .006 264.531); --color-gray-300: oklch(87.2% .01 258.338); --color-gray-400: oklch(70.7% .022 261.325); --color-gray-500: oklch(55.1% .027 264.364); --color-gray-600: oklch(44.6% .03 256.802); --color-gray-700: oklch(37.3% .034 259.733); --color-gray-800: oklch(27.8% .033 256.848); --color-gray-900: oklch(21% .034 264.665); --color-gray-950: oklch(13% .028 261.692); --color-zinc-50: oklch(98.5% 0 none); --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-800: oklch(27.4% .006 286.033); --color-zinc-900: oklch(21% .006 285.885); --color-zinc-950: oklch(14.1% .005 285.823); --color-neutral-50: oklch(98.5% 0 none); --color-neutral-100: oklch(97% 0 none); --color-neutral-200: oklch(92.2% 0 none); --color-neutral-300: oklch(87% 0 none); --color-neutral-400: oklch(70.8% 0 none); --color-neutral-500: oklch(55.6% 0 none); --color-neutral-600: oklch(43.9% 0 none); --color-neutral-700: oklch(37.1% 0 none); --color-neutral-800: oklch(26.9% 0 none); --color-neutral-900: oklch(20.5% 0 none); --color-neutral-950: oklch(14.5% 0 none); --color-stone-50: oklch(98.5% .001 106.423); --color-stone-100: oklch(97% .001 106.424); --color-stone-200: oklch(92.3% .003 48.717); --color-stone-300: oklch(86.9% .005 56.366); --color-stone-400: oklch(70.9% .01 56.259); --color-stone-500: oklch(55.3% .013 58.071); --color-stone-600: oklch(44.4% .011 73.639); --color-stone-700: oklch(37.4% .01 67.558); --color-stone-800: oklch(26.8% .007 34.298); --color-stone-900: oklch(21.6% .006 56.043); --color-stone-950: oklch(14.7% .004 49.25); --color-mauve-50: oklch(98.5% 0 none); --color-mauve-100: oklch(96% .003 325.6); --color-mauve-200: oklch(92.2% .005 325.62); --color-mauve-300: oklch(86.5% .012 325.68); --color-mauve-400: oklch(71.1% .019 323.02); --color-mauve-500: oklch(54.2% .034 322.5); --color-mauve-600: oklch(43.5% .029 321.78); --color-mauve-700: oklch(36.4% .029 323.89); --color-mauve-800: oklch(26.3% .024 320.12); --color-mauve-900: oklch(21.2% .019 322.12); --color-mauve-950: oklch(14.5% .008 326); --color-olive-50: oklch(98.8% .003 106.5); --color-olive-100: oklch(96.6% .005 106.5); --color-olive-200: oklch(93% .007 106.5); --color-olive-300: oklch(88% .011 106.6); --color-olive-400: oklch(73.7% .021 106.9); --color-olive-500: oklch(58% .031 107.3); --color-olive-600: oklch(46.6% .025 107.3); --color-olive-700: oklch(39.4% .023 107.4); --color-olive-800: oklch(28.6% .016 107.4); --color-olive-900: oklch(22.8% .013 107.4); --color-olive-950: oklch(15.3% .006 107.1); --color-mist-50: oklch(98.7% .002 197.1); --color-mist-100: oklch(96.3% .002 197.1); --color-mist-200: oklch(92.5% .005 214.3); --color-mist-300: oklch(87.2% .007 219.6); --color-mist-400: oklch(72.3% .014 214.4); --color-mist-500: oklch(56% .021 213.5); --color-mist-600: oklch(45% .017 213.2); --color-mist-700: oklch(37.8% .015 216); --color-mist-800: oklch(27.5% .011 216.9); --color-mist-900: oklch(21.8% .008 223.9); --color-mist-950: oklch(14.8% .004 228.8); --color-taupe-50: oklch(98.6% .002 67.8); --color-taupe-100: oklch(96% .002 17.2); --color-taupe-200: oklch(92.2% .005 34.3); --color-taupe-300: oklch(86.8% .007 39.5); --color-taupe-400: oklch(71.4% .014 41.2); --color-taupe-500: oklch(54.7% .021 43.1); --color-taupe-600: oklch(43.8% .017 39.3); --color-taupe-700: oklch(36.7% .016 35.7); --color-taupe-800: oklch(26.8% .011 36.5); --color-taupe-900: oklch(21.4% .009 43.1); --color-taupe-950: oklch(14.7% .004 49.3); --color-black: #000; --color-white: #fff; --spacing: .25rem; --breakpoint-sm: 40rem; --breakpoint-md: 48rem; --breakpoint-lg: 64rem; --breakpoint-xl: 80rem; --breakpoint-2xl: 96rem; --container-3xs: 16rem; --container-2xs: 18rem; --container-xs: 20rem; --container-sm: 24rem; --container-md: 28rem; --container-lg: 32rem; --container-xl: 36rem; --container-2xl: 42rem; --container-3xl: 48rem; --container-4xl: 56rem; --container-5xl: 64rem; --container-6xl: 72rem; --container-7xl: 80rem; --text-xs: .75rem; --text-xs--line-height: calc(1/.75); --text-sm: .875rem; --text-sm--line-height: calc(1.25/.875); --text-base: 1rem; --text-base--line-height: calc(1.5/1); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75/1.125); --text-xl: 1.25rem; --text-xl--line-height: calc(1.75/1.25); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2/1.5); --text-3xl: 1.875rem; --text-3xl--line-height: calc(2.25/1.875); --text-4xl: 2.25rem; --text-4xl--line-height: calc(2.5/2.25); --text-5xl: 3rem; --text-5xl--line-height: 1; --text-6xl: 3.75rem; --text-6xl--line-height: 1; --text-7xl: 4.5rem; --text-7xl--line-height: 1; --text-8xl: 6rem; --text-8xl--line-height: 1; --text-9xl: 8rem; --text-9xl--line-height: 1; --font-weight-thin: 100; --font-weight-extralight: 200; --font-weight-light: 300; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --font-weight-extrabold: 800; --font-weight-black: 900; --tracking-tighter: -.05em; --tracking-tight: -.025em; --tracking-normal: 0em; --tracking-wide: .025em; --tracking-wider: .05em; --tracking-widest: .1em; --leading-tight: 1.25; --leading-snug: 1.375; --leading-normal: 1.5; --leading-relaxed: 1.625; --leading-loose: 2; --radius-xs: .125rem; --radius-sm: .25rem; --radius-md: .375rem; --radius-lg: .5rem; --radius-xl: .75rem; --radius-2xl: 1rem; --radius-3xl: 1.5rem; --radius-4xl: 2rem; --shadow-2xs: 0 1px #0000000d; --shadow-xs: 0 1px 2px 0 #0000000d; --shadow-sm: 0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a; --shadow-md: 0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a; --shadow-lg: 0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a; --shadow-xl: 0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a; --shadow-2xl: 0 25px 50px -12px #00000040; --inset-shadow-2xs: inset 0 1px #0000000d; --inset-shadow-xs: inset 0 1px 1px #0000000d; --inset-shadow-sm: inset 0 2px 4px #0000000d; --drop-shadow-xs: 0 1px 1px #0000000d; --drop-shadow-sm: 0 1px 2px #00000026; --drop-shadow-md: 0 3px 3px #0000001f; --drop-shadow-lg: 0 4px 4px #00000026; --drop-shadow-xl: 0 9px 7px #0000001a; --drop-shadow-2xl: 0 25px 25px #00000026; --text-shadow-2xs: 0px 1px 0px #00000026; --text-shadow-xs: 0px 1px 1px #0003; --text-shadow-sm: 0px 1px 0px #00000013,0px 1px 1px #00000013,0px 2px 2px #00000013; --text-shadow-md: 0px 1px 1px #0000001a,0px 1px 2px #0000001a,0px 2px 4px #0000001a; --text-shadow-lg: 0px 1px 2px #0000001a,0px 3px 2px #0000001a,0px 4px 8px #0000001a; --ease-in: cubic-bezier(.4,0,1,1); --ease-out: cubic-bezier(0,0,.2,1); --ease-in-out: cubic-bezier(.4,0,.2,1); --animate-spin: spin 1s linear infinite; --animate-ping: ping 1s cubic-bezier(0,0,.2,1)infinite; --animate-pulse: pulse 2s cubic-bezier(.4,0,.6,1)infinite; --animate-bounce: bounce 1s infinite; @keyframes spin { to { transform: rotate(360deg); }}@keyframes ping { 75%,100% { transform: scale(2); opacity: 0; }}@keyframes pulse { 50% { opacity: .5; }}@keyframes bounce { 0%,100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(.8,0,1,1); }50% { transform: none; animation-timing-function: cubic-bezier(0,0,.2,1); }}--blur-xs: 4px; --blur-sm: 8px; --blur-md: 12px; --blur-lg: 16px; --blur-xl: 24px; --blur-2xl: 40px; --blur-3xl: 64px; --perspective-dramatic: 100px; --perspective-near: 300px; --perspective-normal: 500px; --perspective-midrange: 800px; --perspective-distant: 1200px; --aspect-video: 16/9; --default-transition-duration: .15s; --default-transition-timing-function: cubic-bezier(.4,0,.2,1); --default-font-family: --theme(--font-sans,initial); --default-font-feature-settings: --theme(--font-sans--font-feature-settings,initial); --default-font-variation-settings: --theme(--font-sans--font-variation-settings,initial); --default-mono-font-family: --theme(--font-mono,initial); --default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings,initial); --default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings,initial);}@theme default inline reference{--blur: 8px; --shadow: 0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a; --shadow-inner: inset 0 2px 4px 0 #0000000d; --drop-shadow: 0 1px 2px #0000001a,0 1px 1px #0000000f; --radius: .25rem; --max-width-prose: 65ch;}}@layer base{*,:after,:before{box-sizing:border-box;border:0 solid;margin:0;padding:0}::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::-webkit-file-upload-button{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:--theme(--default-font-family,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:--theme(--default-font-feature-settings,normal);font-variation-settings:--theme(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{color:inherit;border-top-width:1px;height:0}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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:--theme(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:--theme(--default-mono-font-feature-settings,normal);font-variation-settings:--theme(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{position:relative;vertical-align:baseline;font-size:75%;line-height:0}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}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}::-webkit-file-upload-button{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:-webkit-any([multiple],[size])) optgroup{font-weight:bolder}:where(select:-moz-any([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:-webkit-any([multiple],[size])) optgroup option{padding-inline-start:20px}:where(select:-moz-any([multiple],[size])) optgroup option{padding-inline-start:20px}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::-webkit-file-upload-button{margin-inline-end:4px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports ( not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{text-align:inherit;min-height:1lh}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-year-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-month-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-day-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-hour-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-minute-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-second-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-millisecond-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-meridiem-field{padding-block-start:0;padding-block-end:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button{appearance:button}input:where([type=button],[type=reset],[type=submit]){appearance:button}::-webkit-file-upload-button{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 utilities{@tailwind utilities;}:root{--buncss-light: ;--buncss-dark:initial;color-scheme:dark;--page:oklch(.16 .02 307);--surface:oklch(.19 .022 307);--surface-2:oklch(.235 .026 307);--border:oklch(1 0 0/9%);--border-strong:oklch(1 0 0/13%);--text:oklch(.92 .01 307);--muted:oklch(.71 .016 307);--dim:oklch(.53 .018 307);--accent:oklch(.674 .23 341);--accent-fg:oklch(.2 .07 341);--accent-muted:oklch(.674 .23 341/18%);--link:oklch(.817 .112 205);--success:oklch(.76 .14 150);--danger:oklch(.66 .19 25);--warning:oklch(.79 .14 85);--text-primary:var(--text);--text-secondary:var(--muted);--text-muted:var(--dim);--border-subtle:var(--border);--accent-red:var(--danger);--accent-green:var(--success);--accent-cyan:var(--link);--bg-hover:var(--surface-2);--bg-elevated:var(--surface-2);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--radius-sm:3px;--radius-md:6px;--radius-lg:8px}@media (prefers-color-scheme:light){:root:not([data-theme]){--buncss-light:initial;--buncss-dark: ;color-scheme:light;--page:oklch(.985 .004 307);--surface:oklch(1 0 0);--surface-2:oklch(.965 .006 307);--border:oklch(0 0 0/10%);--border-strong:oklch(0 0 0/15%);--text:oklch(.26 .03 307);--muted:oklch(.46 .03 307);--dim:oklch(.58 .025 307);--accent:oklch(.62 .23 341);--accent-fg:oklch(.99 .01 341);--accent-muted:oklch(.62 .23 341/14%);--link:oklch(.58 .13 230);--success:oklch(.55 .13 150);--danger:oklch(.55 .19 25);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--warning:oklch(.6 .13 85)}}[data-theme=light]{--buncss-light:initial;--buncss-dark: ;color-scheme:light;--page:oklch(.985 .004 307);--surface:oklch(1 0 0);--surface-2:oklch(.965 .006 307);--border:oklch(0 0 0/10%);--border-strong:oklch(0 0 0/15%);--text:oklch(.26 .03 307);--muted:oklch(.46 .03 307);--dim:oklch(.58 .025 307);--accent:oklch(.62 .23 341);--accent-fg:oklch(.99 .01 341);--accent-muted:oklch(.62 .23 341/14%);--link:oklch(.58 .13 230);--success:oklch(.55 .13 150);--danger:oklch(.55 .19 25);--select-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--warning:oklch(.6 .13 85)}@layer base{*{box-sizing:border-box}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{background:var(--page);color:var(--text);min-height:100vh;margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}button:focus-visible{outline:2px solid var(--link);outline-offset:2px}a:focus-visible{outline:2px solid var(--link);outline-offset:2px}[tabindex]:focus-visible{outline:2px solid var(--link);outline-offset:2px}}.stats-app-container{display:flex;background:var(--page);color:var(--text);min-height:100vh}.stats-nav-rail{background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;width:240px}.stats-desktop-nav{display:flex}@media (max-width:1023px){.stats-desktop-nav{display:none}}.stats-nav-rail-header{border-bottom:1px solid var(--border);display:flex;align-items: center;height:56px;padding:20px 24px}.stats-logo-container{display:flex;flex-direction:column}.stats-logo-text{letter-spacing:.1em;color:var(--text);font-size:14px;font-weight:700}.stats-logo-subtext{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-size:10px}.stats-nav-rail-menu{display:flex;flex-direction:column;flex:1;gap:4px;padding:16px 12px}.stats-nav-rail-item{display:flex;color:var(--muted);border-radius:var(--radius-md);cursor:pointer;text-align:left;background:0 0;border:none;align-items: center;gap:12px;padding:10px 16px;transition:all .15s;font-size:13px;font-weight:500}.stats-nav-rail-item:hover{color:var(--text);background:var(--surface-2)}.stats-nav-rail-item[data-active=true]{color:var(--accent);background:var(--accent-muted);position:relative}.stats-nav-rail-item[data-active=true]:before{content:"";position:absolute;background:var(--accent);border-radius:0 3px 3px 0;width:3px;height:18px;top:50%;left:0;transform:translateY(-50%)}.stats-nav-rail-item-icon{flex-shrink:0}.stats-nav-rail-footer{border-top:1px solid var(--border);padding:16px 24px}.stats-version-tag{color:var(--dim);font-size:11px}.stats-main-pane{display:flex;flex-direction:column;flex:1;min-width:0}.stats-top-bar{background:var(--surface);border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items: center;height:56px;padding:0 24px}@media (max-width:767px){.stats-top-bar{flex-wrap:wrap;gap:12px;height:auto;min-height:56px;padding:8px 16px}}.stats-top-bar-left{display:flex;align-items: center;gap:12px}.stats-mobile-menu-btn{display:none;color:var(--text);cursor:pointer;background:0 0;border:none;padding:4px}@media (max-width:1023px){.stats-mobile-menu-btn{display:inline-flex;justify-content:center;align-items: center}}.stats-page-title{color:var(--text);margin:0;font-size:16px;font-weight:600}.stats-top-bar-right{display:flex;align-items: center;gap:16px}@media (max-width:767px){.stats-top-bar-right{justify-content:space-between;gap:8px;width:100%}}.stats-top-bar-meta{display:flex;align-items: center}.stats-last-updated{color:var(--dim);font-variant-numeric:tabular-nums;font-size:11px}.stats-content-area{overflow-y:auto;flex:1}.stats-content-inner{display:flex;flex-direction:column;gap:24px;max-width:1560px;margin:0 auto;padding:24px}@media (max-width:767px){.stats-content-inner{gap:16px;padding:16px}}.stats-mobile-drawer-overlay{position:fixed;backdrop-filter:none;z-index:100;background:#0006;inset:0}.stats-mobile-drawer{position:absolute;background:var(--surface);border-right:1px solid var(--border);display:flex;z-index:101;animation:.2s ease-out slideIn;flex-direction:column;width:260px;top:0;bottom:0;left:0}@keyframes slideIn{0%{transform:translate(-100%)}to{transform:translate(0)}}.stats-mobile-drawer-header{display:flex;border-bottom:1px solid var(--border);justify-content:space-between;align-items: center;height:56px;padding:0 16px 0 24px}.stats-mobile-nav{display:flex;height:calc(100% - 56px)}.stats-button{display:inline-flex;border-radius:var(--radius-md);cursor:pointer;border:1px solid #0000;justify-content:center;align-items: center;gap:8px;padding:8px 14px;transition:all .15s;font-family:inherit;font-size:13px;font-weight:500}.stats-button:active:not(:disabled){transform:translateY(1px)}.stats-button-primary{background:var(--accent);color:var(--accent-fg)}.stats-button-primary:hover:not(:disabled){opacity:.9}.stats-button-primary:disabled{opacity:.5;cursor:not-allowed}.stats-button-secondary{background:var(--surface-2);color:var(--text);border-color:var(--border)}.stats-button-secondary:hover:not(:disabled){background:var(--border)}.stats-button-secondary:disabled{opacity:.5;cursor:not-allowed}.stats-range-control{display:flex;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:2px}.stats-range-control-btn{color:var(--muted);border-radius:var(--radius-sm);cursor:pointer;background:0 0;border:none;padding:4px 10px;transition:all .1s;font-size:12px;font-weight:500}.stats-range-control-btn:hover{color:var(--text)}.stats-range-control-btn[data-active=true]{color:var(--text);background:var(--surface);box-shadow:0 1px 2px #0000002e}.stats-select{appearance:none;color:var(--text);background-color:var(--surface-2);background-image:var(--select-chevron);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;text-overflow:ellipsis;background-position:right 10px center;background-repeat:no-repeat;max-width:100%;padding:6px 28px 6px 12px;transition:border-color .15s,background-color .15s;font-family:inherit;font-size:13px;font-weight:500}.stats-select:hover:not(:disabled){border-color:var(--border-strong);background-color:var(--bg-hover)}.stats-select:focus-visible{outline:2px solid var(--link);outline-offset:1px}.stats-select:disabled{opacity:.5;cursor:not-allowed}@media (pointer:coarse){.stats-select{font-size:16px}}.stats-select option{background-color:var(--surface);color:var(--text)}@media (forced-colors:active){.stats-select{appearance:auto}}.stats-sync-container{display:flex;align-items: center;gap:12px}.stats-sync-status-msg{font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;font-size:11px}.stats-sync-status-msg[data-type=success]{color:var(--success)}.stats-sync-status-msg[data-type=error]{color:var(--danger)}.stats-sync-icon{flex-shrink:0}.stats-spin{animation:1s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.stats-panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column}.stats-panel-header{border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items: center;gap:16px;padding:16px 20px}@media (max-width:480px){.stats-panel-header{flex-direction:column;align-items: flex-start;gap:8px}}.stats-panel-header-titles{display:flex;flex-direction:column}.stats-panel-title{color:var(--text);margin:0;font-size:14px;font-weight:600}.stats-panel-subtitle{color:var(--muted);margin:2px 0 0;font-size:12px}.stats-panel-actions{display:flex;align-items: center;gap:12px}.stats-panel-body{padding:20px}.stats-metric-cluster{display:flex;flex-direction:column;gap:16px}.stats-metric-primary-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:16px}@media (max-width:1023px){.stats-metric-primary-grid{grid-template-columns:repeat(2,1fr)}}@media (max-width:480px){.stats-metric-primary-grid{grid-template-columns:1fr}}.stats-metric-secondary-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:12px}@media (max-width:1279px){.stats-metric-secondary-grid{grid-template-columns:repeat(4,1fr)}}@media (max-width:767px){.stats-metric-secondary-grid{grid-template-columns:repeat(2,1fr)}}.stats-metric-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;justify-content:space-between;padding:16px}.stats-metric-card.primary{padding:20px 18px}.stats-metric-card.primary .stats-metric-label{color:var(--accent)}.stats-metric-card.secondary{padding:12px 14px}.stats-metric-label{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:8px;font-size:11px;font-weight:600}.stats-metric-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:20px;font-weight:700}.stats-metric-card.secondary .stats-metric-value{font-size:15px;font-weight:600}.stats-table-wrapper{width:100%}.stats-table-container{overflow-x:auto;width:100%}.stats-table{border-collapse:collapse;font-variant-numeric:tabular-nums;width:100%;font-size:13px}.stats-table-th{text-transform:uppercase;letter-spacing:.05em;color:var(--muted);border-bottom:2px solid var(--border);padding:12px 16px;font-size:11px;font-weight:600}.stats-table-td{border-bottom:1px solid var(--border);color:var(--text);padding:10px 16px}.stats-table-tr{background:0 0}.stats-table-tr-clickable{cursor:pointer;transition:background .12s,box-shadow .12s}.stats-table-tr-clickable:hover{background:var(--surface-2);box-shadow:inset 2px 0 0 var(--accent)}.stats-text-left{text-align:left}.stats-text-right{text-align:right}.stats-table-empty{text-align:center;color:var(--muted);border:1px dashed var(--border);border-radius:var(--radius-lg);padding:40px;font-size:13px}.stats-progress-bar-track{background:var(--surface-2);overflow:hidden;border-radius:999px}.stats-progress-bar-fill{border-radius:inherit;height:100%;transition:width .18s}.stats-progress-bar-fill[data-variant=link]{background:var(--link)}.stats-progress-bar-fill[data-variant=success]{background:var(--success)}.stats-table-mobile-only{display:none}.stats-table-desktop-only{overflow-x:auto;width:100%}@media (max-width:767px){.stats-table-desktop-only{display:none}.stats-table-mobile-only{display:block}}.stats-table-mobile-list{display:flex;flex-direction:column;gap:12px}.stats-table-mobile-card-wrapper{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:14px}.stats-status-pill{display:inline-flex;border-radius:100px;justify-content:center;align-items: center;padding:2px 8px;font-size:11px;font-weight:600;line-height:1.2}.stats-status-pill[data-variant=success]{background:color-mix(in srgb,var(--success)18%,transparent);color:var(--success)}.stats-status-pill[data-variant=danger]{background:color-mix(in srgb,var(--danger)18%,transparent);color:var(--danger)}.stats-status-pill[data-variant=warning]{background:color-mix(in srgb,var(--warning)18%,transparent);color:var(--warning)}.stats-status-pill[data-variant=info]{background:color-mix(in srgb,var(--link)18%,transparent);color:var(--link)}.stats-status-pill[data-variant=default]{background:var(--surface-2);color:var(--muted)}.stats-segmented-control{display:flex;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-md);gap:2px;padding:2px}.stats-segmented-control-btn{color:var(--muted);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:6px 12px;transition:all .1s;font-size:12px;font-weight:500}.stats-segmented-control-btn:hover{color:var(--text)}.stats-segmented-control-btn[data-active=true]{color:var(--text);background:var(--surface);box-shadow:0 1px 2px #0000002e}.stats-drawer-overlay{position:fixed;z-index:100;display:flex;background:#00000080;justify-content:flex-end;inset:0}.stats-drawer{background:var(--surface);border-left:1px solid var(--border);display:flex;animation:.25s cubic-bezier(.16,1,.3,1) drawerSlide;flex-direction:column;width:600px;max-width:100%;height:100%;box-shadow:-4px 0 24px #00000040}@keyframes drawerSlide{0%{transform:translate(100%)}to{transform:translate(0)}}.stats-drawer-header{border-bottom:1px solid var(--border);display:flex;background:var(--surface);flex-shrink:0;justify-content:space-between;align-items: center;height:56px;padding:0 24px}.stats-drawer-header-left{display:flex;align-items:baseline;gap:12px}.stats-drawer-title{color:var(--text);margin:0;font-size:15px;font-weight:600}.stats-drawer-id{color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px}.stats-drawer-close-btn{color:var(--muted);cursor:pointer;display:inline-flex;border-radius:var(--radius-sm);background:0 0;border:none;justify-content:center;align-items: center;padding:4px;transition:all .1s}.stats-drawer-close-btn:hover{color:var(--text);background:var(--surface-2)}.stats-drawer-body{overflow-y:auto;flex:1;padding:24px}.stats-drawer-content{display:flex;flex-direction:column;gap:24px}.stats-drawer-status-card{background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px}.stats-drawer-status-row{display:flex;justify-content:space-between;align-items: flex-start;gap:16px}.stats-drawer-model{color:var(--text);font-size:16px;font-weight:700}.stats-drawer-provider{color:var(--muted);margin-top:2px;font-size:12px}.stats-drawer-error-block{border-top:1px solid var(--border);margin-top:14px;padding-top:12px}.stats-drawer-error-label{text-transform:uppercase;letter-spacing:.05em;color:var(--danger);margin-bottom:4px;font-size:11px;font-weight:600}.stats-drawer-error-text{color:var(--text);white-space:pre-wrap;word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px}.stats-drawer-metrics-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}.stats-drawer-metric-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;justify-content:center;padding:14px}.stats-drawer-metric-label{display:flex;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);align-items: center;gap:6px;margin-bottom:6px;font-size:11px;font-weight:600}.stats-drawer-metric-icon{color:var(--dim)}.stats-drawer-metric-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:16px;font-weight:700}.stats-drawer-metric-sub{color:var(--muted);font-variant-numeric:tabular-nums;margin-top:2px;font-size:11px}.stats-drawer-json-blocks{display:flex;flex-direction:column;gap:16px}.stats-json-block{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface);overflow:hidden}.stats-json-block-header{background:var(--surface-2);border-bottom:1px solid var(--border);display:flex;cursor:pointer;user-select:none;justify-content:space-between;align-items: center;padding:10px 16px;font-size:12px;font-weight:600}.stats-json-block-header:hover{background:var(--border)}.stats-json-block-title{color:var(--text)}.stats-json-block-toggle-indicator{color:var(--muted)}.stats-json-block-content-wrapper{background:var(--page);overflow-x:auto;padding:16px}.stats-json-block-content{color:var(--text);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px;line-height:1.5}.stats-skeleton{background:var(--surface-2);position:relative;overflow:hidden;border-radius:var(--radius-sm)}.stats-skeleton[data-variant=text]{height:14px;margin-top:4px;margin-bottom:4px}.stats-skeleton[data-variant=circle]{border-radius:50%}.stats-skeleton:after{content:"";position:absolute;animation:1.5s infinite shimmer;background:linear-gradient(90deg,#0000 0%,#ffffff0d 50%,#0000 100%);inset:0;transform:translate(-100%)}@media (prefers-color-scheme:light){.stats-skeleton:after{background:linear-gradient(90deg,#0000 0%,#00000008 50%,#0000 100%)}}@keyframes shimmer{to{transform:translate(100%)}}@media (prefers-reduced-motion:reduce){.stats-skeleton:after{animation:none}}.stats-boundary-skeleton{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface);padding:20px}.stats-empty-state,.stats-error-state{display:flex;text-align:center;background:var(--surface);border:1px dashed var(--border);border-radius:var(--radius-lg);flex-direction:column;justify-content:center;align-items: center;padding:48px 24px}.stats-empty-state-message{color:var(--muted);margin:0;font-size:13px}.stats-error-state-content{max-width:360px}.stats-error-state-title{color:var(--text);margin:0 0 8px;font-size:14px;font-weight:600}.stats-error-state-message{color:var(--danger);word-break:break-word;margin:0 0 16px;font-size:12px}.stats-error-state-btn{margin:0 auto}.stats-text-primary{color:var(--text)}.stats-text-secondary{color:var(--muted)}.stats-text-muted{color:var(--dim)}.stats-text-warning{color:var(--warning)}.stats-text-xs{font-size:12px}.stats-font-medium{font-weight:500}.stats-font-semibold{font-weight:600}*{scrollbar-width:thin;scrollbar-color:var(--border-strong)transparent}*::-webkit-scrollbar{width:10px;height:10px}*::-webkit-scrollbar-track{background:0 0}*::-webkit-scrollbar-thumb{background:var(--border-strong);background-clip:padding-box;border:2px solid #0000;border-radius:999px}*::-webkit-scrollbar-thumb:hover{background:var(--dim);background-clip:padding-box;border:2px solid #0000}*::-webkit-scrollbar-corner{background:0 0}@media (prefers-reduced-motion:reduce){.stats-button,.stats-table-tr-clickable,.stats-nav-rail-item,.stats-range-control-btn,.stats-segmented-control-btn{transition:none}.stats-button:active:not(:disabled){transform:none}.stats-mobile-drawer,.stats-drawer{animation:none}}.stats-logo-container{display:grid;grid-template-columns:auto 1fr;align-items: center;column-gap:10px}.stats-logo-container:before{content:"";grid-row:1/3;background:linear-gradient(135deg,#ed4abf 0%,#9b4dff 50%,#5ad8e6 100%);border-radius:4px;width:16px;height:16px}.stats-logo-text,.stats-logo-subtext{grid-column:2}.stats-json-actions{display:flex;align-items: center;gap:12px}.stats-json-copy-btn{display:inline-flex;color:var(--muted);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;background:0 0;align-items: center;gap:5px;padding:3px 8px;transition:color .12s,background .12s;font-family:inherit;font-size:11px;font-weight:600}.stats-json-copy-btn:hover{color:var(--text);background:var(--surface-2)}.stats-empty-state-icon{color:var(--dim);margin-bottom:12px}@media (prefers-reduced-motion:reduce){.stats-json-copy-btn{transition:none}}.stats-theme-toggle{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface);color:var(--muted);cursor:pointer;flex-shrink:0;justify-content:center;align-items: center;width:32px;height:32px;transition:color .14s,background .14s,border-color .14s}.stats-theme-toggle:hover{color:var(--text);border-color:var(--border-strong)}@media (prefers-reduced-motion:reduce){.stats-theme-toggle{transition:none}}.stats-trace-view{display:flex;flex-direction:column;gap:14px}.stats-trace-header{display:flex;flex-wrap:wrap;align-items: center;gap:12px;min-width:0}.stats-trace-back{display:inline-flex;color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;align-items: center;gap:6px;padding:6px 12px;transition:color .14s,border-color .14s;font-size:12px}.stats-trace-back:hover{color:var(--text);border-color:var(--border-strong)}.stats-trace-back:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-title{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;margin:0;font-size:15px;font-weight:600}.stats-trace-cwd{color:var(--dim);border:1px solid var(--border);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:999px;max-width:280px;padding:3px 10px;font-size:11px}.stats-trace-summary{display:flex;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;flex-wrap:wrap}.stats-trace-summary-cell{display:flex;border-right:1px solid var(--border);flex-direction:column;gap:3px;min-width:76px;padding:10px 16px}.stats-trace-summary-cell:last-child{border-right:none}.stats-trace-summary-label{text-transform:uppercase;letter-spacing:.07em;color:var(--dim);font-size:9px}.stats-trace-summary-value{color:var(--text);font-variant-numeric:tabular-nums;font-size:13px;font-weight:500}.stats-trace-toolbar{display:flex;flex-wrap:wrap;align-items: center;gap:12px}.stats-trace-toolbar-hint{color:var(--dim);margin-left:auto;font-size:10px}.stats-trace-input{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text);padding:6px 10px;font-family:inherit;font-size:12px}@media (pointer:coarse){.stats-trace-input{font-size:16px}}.stats-trace-input::-webkit-search-cancel-button{filter:invert(1)}@media (prefers-color-scheme:light){:root:not([data-theme]) .stats-trace-input::-webkit-search-cancel-button{filter:none}}[data-theme=light] .stats-trace-input::-webkit-search-cancel-button{filter:none}.stats-trace-input:focus-visible{outline:2px solid var(--link);outline-offset:1px}.stats-trace-input::placeholder{color:var(--dim)}.stats-trace-check{display:inline-flex;color:var(--muted);cursor:pointer;user-select:none;align-items: center;gap:6px;font-size:12px}.stats-trace-check input[type=checkbox]{accent-color:var(--accent)}.stats-trace-icon-btn{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-md);color:var(--muted);cursor:pointer;background:0 0;justify-content:center;align-items: center;width:26px;height:26px;transition:color .14s,border-color .14s}.stats-trace-icon-btn:hover:not(:disabled){color:var(--text);border-color:var(--border-strong)}.stats-trace-icon-btn:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-icon-btn:disabled{opacity:.4;cursor:default}.stats-trace-timeline-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;gap:10px;padding:12px}.stats-trace-timeline{position:relative;display:flex;overflow-y:auto;overflow-x:hidden;max-height:560px}.stats-trace-canvas{cursor:grab;outline:none;touch-action:none;border-radius:var(--radius-sm)}.stats-trace-canvas:focus-visible{outline:2px solid var(--link);outline-offset:2px}.stats-trace-canvas:active{cursor:grabbing}.stats-trace-chevron{display:inline-flex;cursor:pointer;color:var(--muted);background:0 0;border:none;padding:0}.stats-trace-chevron:hover{color:var(--text)}.stats-trace-gutter-track{color:var(--text);font-size:11px;font-weight:500}.stats-trace-gutter-model{color:var(--dim);font-size:9px}.stats-trace-gutter-lane{position:absolute;color:var(--dim);text-transform:uppercase;letter-spacing:.08em;font-size:8px}.stats-trace-list{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);overflow-y:auto;height:340px}.stats-trace-row{display:flex;border:none;border-bottom:1px solid var(--border);text-align:left;cursor:pointer;background:0 0;align-items: center;gap:10px;width:100%;height:28px;padding:0 12px;transition:background .1s}.stats-trace-row:hover{background:var(--bg-hover)}.stats-trace-row:focus-visible{outline:2px solid var(--link);outline-offset:-2px}.stats-trace-row[data-selected=true]{background:var(--accent-muted)}.stats-trace-row-static{cursor:default}.stats-trace-row-static:hover{background:0 0}.stats-trace-kind{text-transform:uppercase;letter-spacing:.06em;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:3px;flex-shrink:0;width:58px;padding:2px 0;font-size:8px}.stats-trace-row-meta{color:var(--dim);font-variant-numeric:tabular-nums;display:flex;flex-shrink:0;gap:10px;margin-left:auto;font-size:10px}@media (prefers-reduced-motion:reduce){.stats-trace-back,.stats-trace-icon-btn,.stats-trace-row{transition:none}}
@@ -1732,9 +1732,26 @@
1732
1732
  border: 1px solid var(--border);
1733
1733
  border-radius: var(--radius-md);
1734
1734
  color: var(--text);
1735
+ font-family: inherit;
1735
1736
  font-size: 12px;
1736
1737
  padding: 6px 10px;
1737
1738
  }
1739
+ @media (pointer: coarse) {
1740
+ .stats-trace-input {
1741
+ font-size: 16px;
1742
+ }
1743
+ }
1744
+ .stats-trace-input::-webkit-search-cancel-button {
1745
+ filter: invert(1);
1746
+ }
1747
+ @media (prefers-color-scheme: light) {
1748
+ :root:not([data-theme]) .stats-trace-input::-webkit-search-cancel-button {
1749
+ filter: none;
1750
+ }
1751
+ }
1752
+ [data-theme="light"] .stats-trace-input::-webkit-search-cancel-button {
1753
+ filter: none;
1754
+ }
1738
1755
  .stats-trace-input:focus-visible {
1739
1756
  outline: 2px solid var(--link);
1740
1757
  outline-offset: 1px;
@@ -1751,6 +1768,9 @@
1751
1768
  cursor: pointer;
1752
1769
  user-select: none;
1753
1770
  }
1771
+ .stats-trace-check input[type="checkbox"] {
1772
+ accent-color: var(--accent);
1773
+ }
1754
1774
  .stats-trace-icon-btn {
1755
1775
  display: inline-flex;
1756
1776
  align-items: center;
@@ -1,4 +1,5 @@
1
1
  import { Database } from "bun:sqlite";
2
+ import { type ParseSessionResult, type SessionParserState } from "./parser.js";
2
3
  import type { AgentTypeStats, AggregatedStats, BehaviorModelStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, DailyActivityPoint, FolderStats, MessageStats, MessageStatsInput, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, ProviderAggregate, ProviderHourlyPoint, ProviderTimeSeriesPoint, TimeSeriesPoint, ToolCallStats, ToolModelStats, ToolResultLink, ToolTimeSeriesPoint, ToolUsageStats, UserMessageLink, UserMessageStats } from "./types.js";
3
4
  /**
4
5
  * Initialize the database and create tables.
@@ -12,11 +13,18 @@ export declare function isScheduledCatalogModel(provider: string, modelId: strin
12
13
  export declare function getFileOffset(sessionFile: string): {
13
14
  offset: number;
14
15
  lastModified: number;
16
+ parserState?: SessionParserState;
15
17
  } | null;
16
18
  /**
17
19
  * Update the stored offset for a session file.
18
20
  */
19
- export declare function setFileOffset(sessionFile: string, offset: number, lastModified: number): void;
21
+ export declare function setFileOffset(sessionFile: string, offset: number, lastModified: number, parserState?: SessionParserState): void;
22
+ export declare function applySessionParseResult(sessionFile: string, result: ParseSessionResult, rebuild?: boolean): {
23
+ processed: number;
24
+ reconcile: boolean;
25
+ };
26
+ export declare function prepareSessionSync(): boolean;
27
+ export declare function completeSessionSync(reconcile: boolean): void;
20
28
  /**
21
29
  * Insert message stats into the database.
22
30
  *
@@ -1,3 +1,5 @@
1
+ import type * as nodeFs from "node:fs";
2
+ import { type ServiceTierByFamily } from "@oh-my-pi/pi-ai";
1
3
  import type { AgentType, MessageStatsInput, SessionEntry, ToolCallStats, ToolResultLink, UserMessageLink, UserMessageStats } from "./types.js";
2
4
  /**
3
5
  * Classify which agent produced a transcript from its path within the sessions
@@ -44,22 +46,17 @@ export interface UsageBucketView {
44
46
  export declare function resolveUsageTotal(usage: UsageBucketView | null | undefined): number;
45
47
  /** Parse every well-formed entry in a transcript buffer (malformed lines skipped). */
46
48
  export declare function parseAllSessionEntries(bytes: Uint8Array): SessionEntry[];
47
- /**
48
- * Parse a session file and extract all assistant message stats.
49
- * Uses incremental reading with offset tracking.
50
- *
51
- * Service-tier carry-over: `currentServiceTier` is a session-scoped piece of
52
- * state derived from `service_tier_change` entries that affects whether
53
- * subsequent OpenAI assistant replies count as premium requests. Incremental
54
- * syncs that resume past the most-recent tier change would otherwise lose
55
- * that state and silently record `premiumRequests = 0` for priority traffic
56
- * (the coding-agent stopped folding the tier into `usage.premiumRequests`
57
- * after 13f59162e — the parser is now the sole source of truth). When
58
- * `fromOffset > 0` we therefore scan the bytes preceding `fromOffset`
59
- * for the latest service-tier value before parsing the unprocessed tail.
60
- * The scan only keeps the current tier and does not materialize prefix
61
- * entries, preserving offset-based memory behavior for large sessions.
62
- */
49
+ export interface SessionParserState {
50
+ version: 1;
51
+ offset: number;
52
+ dev: number;
53
+ ino: number;
54
+ birthtimeMs: number;
55
+ size: number;
56
+ mtimeMs: number;
57
+ checkpoint: string;
58
+ serviceTier: ServiceTierByFamily | null;
59
+ }
63
60
  export interface ParseSessionResult {
64
61
  stats: MessageStatsInput[];
65
62
  userStats: UserMessageStats[];
@@ -67,8 +64,12 @@ export interface ParseSessionResult {
67
64
  toolCalls: ToolCallStats[];
68
65
  toolResults: ToolResultLink[];
69
66
  newOffset: number;
67
+ parserState?: SessionParserState;
68
+ reset?: boolean;
70
69
  }
71
- export declare function parseSessionFile(sessionPath: string, fromOffset?: number): Promise<ParseSessionResult>;
70
+ export declare function matchesSessionFile(state: SessionParserState, info: nodeFs.Stats): boolean;
71
+ /** Offset-only callers reconstruct service-tier state once; persisted cursors read only the appended tail. */
72
+ export declare function parseSessionFile(sessionPath: string, fromOffset?: number, state?: SessionParserState, replay?: boolean): Promise<ParseSessionResult>;
72
73
  /**
73
74
  * List all session directories (folders).
74
75
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Stateless parse worker for `syncAllSessions`. The main thread owns the
3
- * SQLite handle; workers receive `{ sessionFile, fromOffset }`, run
3
+ * SQLite handle; workers receive a session path, offset, and parser state, run
4
4
  * `parseSessionFile` (which is pure I/O + CPU, no DB), and post the
5
5
  * structured-clone-safe result back. One in-flight request per worker so
6
6
  * the main thread can fan jobs out 1:1 with the pool size.
@@ -10,11 +10,13 @@
10
10
  * worker actually spawns and runs in compiled binaries (regression coverage
11
11
  * for issue #1011 / PR #1027, where the worker silently failed to load).
12
12
  */
13
- import { type ParseSessionResult } from "./parser.js";
13
+ import { type ParseSessionResult, type SessionParserState } from "./parser.js";
14
14
  export type SyncWorkerRequest = {
15
15
  kind?: "parse";
16
16
  sessionFile: string;
17
17
  fromOffset: number;
18
+ parserState?: SessionParserState;
19
+ replay?: boolean;
18
20
  } | {
19
21
  kind: "ping";
20
22
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/omp-stats",
4
- "version": "18.2.0",
4
+ "version": "18.2.1",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -39,9 +39,9 @@
39
39
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "18.2.0",
43
- "@oh-my-pi/pi-catalog": "18.2.0",
44
- "@oh-my-pi/pi-utils": "18.2.0",
42
+ "@oh-my-pi/pi-ai": "18.2.1",
43
+ "@oh-my-pi/pi-catalog": "18.2.1",
44
+ "@oh-my-pi/pi-utils": "18.2.1",
45
45
  "@tailwindcss/node": "^4.3.2",
46
46
  "chart.js": "^4.5.1",
47
47
  "lucide-react": "^1.24.0",
package/src/aggregator.ts CHANGED
@@ -3,6 +3,8 @@ import * as path from "node:path";
3
3
  import { getStatsDbPath, workerHostEntry } from "@oh-my-pi/pi-utils";
4
4
  import { withFileLock } from "@oh-my-pi/pi-utils/file-lock";
5
5
  import {
6
+ applySessionParseResult,
7
+ completeSessionSync,
6
8
  getRecentErrors as dbGetRecentErrors,
7
9
  getRecentRequests as dbGetRecentRequests,
8
10
  getBehaviorByModel,
@@ -26,15 +28,17 @@ import {
26
28
  getToolStatsByModel,
27
29
  getToolTimeSeries,
28
30
  initDb,
29
- insertMessageStats,
30
- insertToolCalls,
31
- insertUserMessageStats,
32
31
  markSessionBackfillsComplete,
33
- setFileOffset,
34
- updateToolResults,
35
- updateUserMessageLinks,
32
+ prepareSessionSync,
36
33
  } from "./db";
37
- import { getSessionEntry, listAllSessionFiles, type ParseSessionResult, parseSessionFile } from "./parser";
34
+ import {
35
+ getSessionEntry,
36
+ listAllSessionFiles,
37
+ matchesSessionFile,
38
+ type ParseSessionResult,
39
+ parseSessionFile,
40
+ type SessionParserState,
41
+ } from "./parser";
38
42
  import type { SyncWorkerRequest, SyncWorkerResponse } from "./sync-worker";
39
43
  // Coding-agent binary/bundle workers route through the CLI entrypoint with a
40
44
  // hidden argv mode, so the compiled binary and npm bundle only need one
@@ -69,20 +73,6 @@ export async function withStatsSyncLock<T>(dbPath: string, fn: () => Promise<T>)
69
73
  });
70
74
  }
71
75
 
72
- /**
73
- * Apply a freshly parsed result to the database. Runs entirely on the
74
- * main thread so the single SQLite handle owns every write.
75
- */
76
- function applyParseResult(sessionFile: string, lastModified: number, result: ParseSessionResult): number {
77
- if (result.stats.length > 0) insertMessageStats(result.stats);
78
- if (result.userStats.length > 0) insertUserMessageStats(result.userStats);
79
- if (result.userLinks.length > 0) updateUserMessageLinks(result.userLinks);
80
- if (result.toolCalls.length > 0) insertToolCalls(result.toolCalls);
81
- if (result.toolResults.length > 0) updateToolResults(result.toolResults);
82
- setFileOffset(sessionFile, result.newOffset, lastModified);
83
- return result.stats.length + result.userStats.length;
84
- }
85
-
86
76
  /**
87
77
  * Progress event emitted after each session file is fully processed.
88
78
  * `current` is the number of files completed (skipped + parsed),
@@ -239,20 +229,34 @@ export async function smokeTestSyncWorker({ timeoutMs = 5_000 }: { timeoutMs?: n
239
229
  * bar walks at a steady rate).
240
230
  */
241
231
  export async function syncAllSessions(opts?: SyncOptions): Promise<{ processed: number; files: number }> {
242
- return withStatsSyncLock(getStatsDbPath(), () => syncAllSessionsLocked(opts));
232
+ return withStatsSyncLock(getStatsDbPath(), async () => {
233
+ let processed = 0;
234
+ let files = 0;
235
+ while (true) {
236
+ const result = await syncAllSessionsLocked(opts);
237
+ processed += result.processed;
238
+ files += result.files;
239
+ if (!result.reconcile) return { processed, files };
240
+ }
241
+ });
243
242
  }
244
243
 
245
- async function syncAllSessionsLocked(opts?: SyncOptions): Promise<{ processed: number; files: number }> {
244
+ async function syncAllSessionsLocked(
245
+ opts?: SyncOptions,
246
+ ): Promise<{ processed: number; files: number; reconcile: boolean }> {
246
247
  await initDb();
248
+ const replay = prepareSessionSync();
247
249
 
248
250
  const files = await listAllSessionFiles();
249
251
  let totalProcessed = 0;
250
252
  let filesProcessed = 0;
251
253
  let completed = 0;
252
254
  let cursor = 0;
255
+ let reconcile = false;
253
256
  const finish = () => {
257
+ completeSessionSync(reconcile);
254
258
  markSessionBackfillsComplete();
255
- return { processed: totalProcessed, files: filesProcessed };
259
+ return { processed: totalProcessed, files: filesProcessed, reconcile };
256
260
  };
257
261
  if (files.length === 0) return finish();
258
262
 
@@ -268,7 +272,12 @@ async function syncAllSessionsLocked(opts?: SyncOptions): Promise<{ processed: n
268
272
 
269
273
  const processFile = async (
270
274
  sessionFile: string,
271
- parse: (sessionFile: string, fromOffset: number) => Promise<ParseSessionResult>,
275
+ parse: (
276
+ sessionFile: string,
277
+ fromOffset: number,
278
+ state?: SessionParserState,
279
+ replay?: boolean,
280
+ ) => Promise<ParseSessionResult>,
272
281
  ): Promise<void> => {
273
282
  let fileStats: fs.Stats;
274
283
  try {
@@ -279,14 +288,24 @@ async function syncAllSessionsLocked(opts?: SyncOptions): Promise<{ processed: n
279
288
  }
280
289
  const lastModified = fileStats.mtimeMs;
281
290
  const stored = getFileOffset(sessionFile);
282
- if (stored && stored.lastModified >= lastModified) {
291
+ if (
292
+ !replay &&
293
+ stored?.parserState &&
294
+ stored.lastModified === lastModified &&
295
+ stored.parserState.size === fileStats.size &&
296
+ matchesSessionFile(stored.parserState, fileStats)
297
+ ) {
283
298
  report(sessionFile);
284
299
  return;
285
300
  }
286
301
 
287
- const fromOffset = stored?.offset ?? 0;
288
- const result = await parse(sessionFile, fromOffset);
289
- const inserted = applyParseResult(sessionFile, lastModified, result);
302
+ const unknownIdentity = stored !== null && !stored.parserState;
303
+ const fromOffset = unknownIdentity ? 0 : (stored?.offset ?? 0);
304
+ const result = await parse(sessionFile, fromOffset, stored?.parserState, replay);
305
+ if (unknownIdentity && result.parserState) result.reset = true;
306
+ const applied = applySessionParseResult(sessionFile, result, replay || !stored?.parserState);
307
+ const inserted = applied.processed;
308
+ if (applied.reconcile) reconcile = true;
290
309
  if (inserted > 0) {
291
310
  totalProcessed += inserted;
292
311
  filesProcessed++;
@@ -312,7 +331,9 @@ async function syncAllSessionsLocked(opts?: SyncOptions): Promise<{ processed: n
312
331
  const idx = cursor++;
313
332
  if (idx >= files.length) return;
314
333
  const sessionFile = files[idx];
315
- await processFile(sessionFile, (file, fromOffset) => dispatch(handle, { sessionFile: file, fromOffset }));
334
+ await processFile(sessionFile, (file, fromOffset, parserState, replay) =>
335
+ dispatch(handle, { sessionFile: file, fromOffset, parserState, replay }),
336
+ );
316
337
  }
317
338
  }
318
339
 
@@ -1509,10 +1509,35 @@
1509
1509
  border: 1px solid var(--border);
1510
1510
  border-radius: var(--radius-md);
1511
1511
  color: var(--text);
1512
+ font-family: inherit;
1512
1513
  font-size: 12px;
1513
1514
  padding: 6px 10px;
1514
1515
  }
1515
1516
 
1517
+ /* Prevent iOS from zooming the viewport when a search input receives focus
1518
+ (mirrors the .stats-select coarse-pointer treatment). */
1519
+ @media (pointer: coarse) {
1520
+ .stats-trace-input {
1521
+ font-size: 16px;
1522
+ }
1523
+ }
1524
+
1525
+ /* Native clear button is a dark glyph in WebKit; keep it visible in the dark
1526
+ theme by inverting it (no-op in light). */
1527
+ .stats-trace-input::-webkit-search-cancel-button {
1528
+ filter: invert(1);
1529
+ }
1530
+
1531
+ @media (prefers-color-scheme: light) {
1532
+ :root:not([data-theme]) .stats-trace-input::-webkit-search-cancel-button {
1533
+ filter: none;
1534
+ }
1535
+ }
1536
+
1537
+ [data-theme="light"] .stats-trace-input::-webkit-search-cancel-button {
1538
+ filter: none;
1539
+ }
1540
+
1516
1541
  .stats-trace-input:focus-visible {
1517
1542
  outline: 2px solid var(--link);
1518
1543
  outline-offset: 1px;
@@ -1532,6 +1557,11 @@
1532
1557
  user-select: none;
1533
1558
  }
1534
1559
 
1560
+ /* Brand-tint the native checkbox instead of leaving the browser default. */
1561
+ .stats-trace-check input[type="checkbox"] {
1562
+ accent-color: var(--accent);
1563
+ }
1564
+
1535
1565
  .stats-trace-icon-btn {
1536
1566
  display: inline-flex;
1537
1567
  align-items: center;
package/src/db.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  } from "@oh-my-pi/pi-catalog/models";
10
10
  import type { ModelCost } from "@oh-my-pi/pi-catalog/types";
11
11
  import { getConfigRootDir, getStatsDbPath } from "@oh-my-pi/pi-utils";
12
- import { classifyAgentType } from "./parser";
12
+ import { classifyAgentType, type ParseSessionResult, type SessionParserState } from "./parser";
13
13
  import type {
14
14
  AgentType,
15
15
  AgentTypeStats,
@@ -144,7 +144,10 @@ const USER_MESSAGE_LINKS_REPAIR_KEY = "user_message_links_v1";
144
144
  const PRIORITY_PREMIUM_REQUESTS_BACKFILL_KEY = "premium_requests_priority_v1";
145
145
  const AGENT_TYPE_BACKFILL_KEY = "agent_type_v1";
146
146
  const FORK_DEDUPE_KEY = "fork_dedupe_v1";
147
- const TOOL_CALLS_BACKFILL_KEY = "tool_calls_v1";
147
+ // v2: tool-name sanitization at ingest (see `sanitizeToolName` in parser.ts)
148
+ // collapses provider-side garbage names; a full re-parse replaces the polluted
149
+ // rows already stored in existing databases.
150
+ const TOOL_CALLS_BACKFILL_KEY = "tool_calls_v2";
148
151
  // Older ingests dropped `Usage.orchestration` (never a stored column) when
149
152
  // pricing, so subscription models billed on orchestration tokens — multi-agent
150
153
  // Grok most notably — were priced from conversation buckets alone and could not
@@ -222,7 +225,8 @@ export async function initDb(): Promise<Database> {
222
225
  CREATE TABLE IF NOT EXISTS file_offsets (
223
226
  session_file TEXT PRIMARY KEY,
224
227
  offset INTEGER NOT NULL,
225
- last_modified INTEGER NOT NULL
228
+ last_modified INTEGER NOT NULL,
229
+ parser_state TEXT
226
230
  );
227
231
 
228
232
  CREATE TABLE IF NOT EXISTS user_messages (
@@ -274,6 +278,10 @@ export async function initDb(): Promise<Database> {
274
278
  );
275
279
  `);
276
280
 
281
+ const offsetColumns = db.prepare("PRAGMA table_info(file_offsets)").all() as { name: string }[];
282
+ if (!offsetColumns.some(column => column.name === "parser_state")) {
283
+ db.run("ALTER TABLE file_offsets ADD COLUMN parser_state TEXT");
284
+ }
277
285
  const messageColumns = db.prepare("PRAGMA table_info(messages)").all() as { name: string }[];
278
286
  if (!messageColumns.some(column => column.name === "premium_requests")) {
279
287
  db.run("ALTER TABLE messages ADD COLUMN premium_requests REAL NOT NULL DEFAULT 0");
@@ -590,26 +598,104 @@ function backfillNoCacheInputCosts(database: Database): void {
590
598
  /**
591
599
  * Get the stored offset for a session file.
592
600
  */
593
- export function getFileOffset(sessionFile: string): { offset: number; lastModified: number } | null {
601
+ export function getFileOffset(
602
+ sessionFile: string,
603
+ ): { offset: number; lastModified: number; parserState?: SessionParserState } | null {
594
604
  if (!db) return null;
595
605
 
596
- const stmt = db.prepare("SELECT offset, last_modified FROM file_offsets WHERE session_file = ?");
597
- const row = stmt.get(sessionFile) as { offset: number; last_modified: number } | undefined;
598
-
599
- return row ? { offset: row.offset, lastModified: row.last_modified } : null;
606
+ const stmt = db.prepare("SELECT offset, last_modified, parser_state FROM file_offsets WHERE session_file = ?");
607
+ const row = stmt.get(sessionFile) as
608
+ | { offset: number; last_modified: number; parser_state: string | null }
609
+ | undefined;
610
+ if (!row) return null;
611
+ let parserState: SessionParserState | undefined;
612
+ if (row.parser_state) {
613
+ try {
614
+ const state = JSON.parse(row.parser_state) as SessionParserState;
615
+ if (state?.version === 1 && state.offset === row.offset) parserState = state;
616
+ } catch {
617
+ /* A missing cursor is reconstructed from the transcript. */
618
+ }
619
+ }
620
+ return { offset: row.offset, lastModified: row.last_modified, parserState };
600
621
  }
601
622
 
602
623
  /**
603
624
  * Update the stored offset for a session file.
604
625
  */
605
- export function setFileOffset(sessionFile: string, offset: number, lastModified: number): void {
626
+ export function setFileOffset(
627
+ sessionFile: string,
628
+ offset: number,
629
+ lastModified: number,
630
+ parserState?: SessionParserState,
631
+ ): void {
606
632
  if (!db) return;
607
633
 
608
634
  const stmt = db.prepare(`
609
- INSERT OR REPLACE INTO file_offsets (session_file, offset, last_modified)
610
- VALUES (?, ?, ?)
635
+ INSERT OR REPLACE INTO file_offsets (session_file, offset, last_modified, parser_state)
636
+ VALUES (?, ?, ?, ?)
611
637
  `);
612
- stmt.run(sessionFile, offset, lastModified);
638
+ stmt.run(sessionFile, offset, lastModified, parserState ? JSON.stringify(parserState) : null);
639
+ }
640
+
641
+ export function applySessionParseResult(
642
+ sessionFile: string,
643
+ result: ParseSessionResult,
644
+ rebuild = false,
645
+ ): { processed: number; reconcile: boolean } {
646
+ const parserState = result.parserState;
647
+ if (!db || !parserState) return { processed: 0, reconcile: false };
648
+ const database = db;
649
+ return database.transaction(() => {
650
+ let reconcile = result.reset ?? false;
651
+ if (result.reset || rebuild) {
652
+ const retainedMessages = new Set(result.stats.map(row => JSON.stringify([row.entryId, row.timestamp])));
653
+ const retainedUsers = new Set(result.userStats.map(row => JSON.stringify([row.entryId, row.timestamp])));
654
+ const retainedTools = new Set(
655
+ result.toolCalls.map(row => JSON.stringify([row.entryId, row.timestamp, row.toolCallId])),
656
+ );
657
+ const messages = database
658
+ .prepare("SELECT entry_id, timestamp FROM messages WHERE session_file = ?")
659
+ .all(sessionFile) as {
660
+ entry_id: string;
661
+ timestamp: number;
662
+ }[];
663
+ const users = database
664
+ .prepare("SELECT entry_id, timestamp FROM user_messages WHERE session_file = ?")
665
+ .all(sessionFile) as { entry_id: string; timestamp: number }[];
666
+ const tools = database
667
+ .prepare("SELECT entry_id, timestamp, tool_call_id FROM tool_calls WHERE session_file = ?")
668
+ .all(sessionFile) as { entry_id: string; timestamp: number; tool_call_id: string }[];
669
+ // A removed owner may have surviving fork copies skipped earlier in this pass.
670
+ reconcile ||=
671
+ messages.some(row => !retainedMessages.has(JSON.stringify([row.entry_id, row.timestamp]))) ||
672
+ users.some(row => !retainedUsers.has(JSON.stringify([row.entry_id, row.timestamp]))) ||
673
+ tools.some(row => !retainedTools.has(JSON.stringify([row.entry_id, row.timestamp, row.tool_call_id])));
674
+ database.prepare("DELETE FROM messages WHERE session_file = ?").run(sessionFile);
675
+ database.prepare("DELETE FROM user_messages WHERE session_file = ?").run(sessionFile);
676
+ database.prepare("DELETE FROM tool_calls WHERE session_file = ?").run(sessionFile);
677
+ }
678
+ if (reconcile) {
679
+ database
680
+ .prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('session_reconciliation', 'pending')")
681
+ .run();
682
+ }
683
+ if (result.stats.length > 0) insertMessageStats(result.stats);
684
+ if (result.userStats.length > 0) insertUserMessageStats(result.userStats);
685
+ if (result.userLinks.length > 0) updateUserMessageLinks(result.userLinks);
686
+ if (result.toolCalls.length > 0) insertToolCalls(result.toolCalls);
687
+ if (result.toolResults.length > 0) updateToolResults(result.toolResults);
688
+ setFileOffset(sessionFile, result.newOffset, parserState.mtimeMs, parserState);
689
+ return { processed: result.stats.length + result.userStats.length, reconcile };
690
+ })();
691
+ }
692
+
693
+ export function prepareSessionSync(): boolean {
694
+ return Boolean(db?.prepare("SELECT 1 FROM meta WHERE key = 'session_reconciliation'").get());
695
+ }
696
+
697
+ export function completeSessionSync(reconcile: boolean): void {
698
+ if (!reconcile) db?.prepare("DELETE FROM meta WHERE key = 'session_reconciliation'").run();
613
699
  }
614
700
 
615
701
  /**
package/src/parser.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type * as nodeFs from "node:fs";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import {
@@ -29,6 +30,9 @@ import { computeUserMessageMetrics } from "./user-metrics";
29
30
  /** Basename of an advisor agent's transcript inside a session artifacts dir. */
30
31
  const ADVISOR_TRANSCRIPT_BASENAME = "__advisor.jsonl";
31
32
 
33
+ /** Characters a persisted tool name may consist of without sanitization. */
34
+ const TOOL_NAME_PATTERN = /^[\w.:-]+$/;
35
+
32
36
  /**
33
37
  * Classify which agent produced a transcript from its path within the sessions
34
38
  * directory. Layout: `<sessionsDir>/<project>/<file>.jsonl` is the `main`
@@ -354,27 +358,51 @@ function extractToolCalls(
354
358
  );
355
359
  if (blocks.length === 0) return [];
356
360
 
357
- return blocks.map(block => {
361
+ const calls: ToolCallStats[] = [];
362
+ for (const block of blocks) {
363
+ // Names reduced to nothing by sanitization carry no tool identity:
364
+ // skip them rather than attributing usage to garbage (see
365
+ // sanitizeToolName). callsInTurn still counts the raw block total.
366
+ const toolName = sanitizeToolName(block.name);
367
+ if (toolName === null) continue;
358
368
  let argsChars = 0;
359
369
  try {
360
370
  argsChars = JSON.stringify(block.arguments ?? {}).length;
361
371
  } catch {
362
372
  // Non-serializable arguments (shouldn't happen in persisted JSONL); size unknown.
363
373
  }
364
- return {
374
+ calls.push({
365
375
  sessionFile,
366
376
  entryId: entry.id,
367
377
  toolCallId: block.id,
368
378
  folder,
369
- toolName: block.name,
379
+ toolName,
370
380
  model: msg.model,
371
381
  provider: msg.provider,
372
382
  timestamp: coerceEntryTimestamp(msg.timestamp, entry),
373
383
  agentType,
374
384
  callsInTurn: blocks.length,
375
385
  argsChars,
376
- };
377
- });
386
+ });
387
+ }
388
+ return calls;
389
+ }
390
+
391
+ /**
392
+ * Tool names as persisted can be polluted by provider-side parse garbage — a
393
+ * gateway may hand the model's whole invocation text back as the function
394
+ * name (e.g. `bash command="ls -la …"` with a stray in-band closer), which
395
+ * then shows up verbatim in every `GROUP BY tool_name` aggregate and the
396
+ * dashboard tool filter. Reduce such names to their leading identifier token;
397
+ * names that yield no identifier at all carry no tool identity and are
398
+ * returned as `null` so the row is skipped.
399
+ */
400
+ function sanitizeToolName(name: string): string | null {
401
+ const trimmed = name.trim();
402
+ if (trimmed.length === 0) return null;
403
+ if (TOOL_NAME_PATTERN.test(trimmed)) return trimmed;
404
+ const candidate = trimmed.split(/[^\w.:-]/)[0] ?? "";
405
+ return candidate.length > 0 ? candidate : null;
378
406
  }
379
407
 
380
408
  /**
@@ -454,22 +482,18 @@ function scanLastServiceTier(bytes: Uint8Array): ServiceTierByFamily | undefined
454
482
  });
455
483
  return currentServiceTier;
456
484
  }
457
- /**
458
- * Parse a session file and extract all assistant message stats.
459
- * Uses incremental reading with offset tracking.
460
- *
461
- * Service-tier carry-over: `currentServiceTier` is a session-scoped piece of
462
- * state derived from `service_tier_change` entries that affects whether
463
- * subsequent OpenAI assistant replies count as premium requests. Incremental
464
- * syncs that resume past the most-recent tier change would otherwise lose
465
- * that state and silently record `premiumRequests = 0` for priority traffic
466
- * (the coding-agent stopped folding the tier into `usage.premiumRequests`
467
- * after 13f59162e — the parser is now the sole source of truth). When
468
- * `fromOffset > 0` we therefore scan the bytes preceding `fromOffset`
469
- * for the latest service-tier value before parsing the unprocessed tail.
470
- * The scan only keeps the current tier and does not materialize prefix
471
- * entries, preserving offset-based memory behavior for large sessions.
472
- */
485
+ export interface SessionParserState {
486
+ version: 1;
487
+ offset: number;
488
+ dev: number;
489
+ ino: number;
490
+ birthtimeMs: number;
491
+ size: number;
492
+ mtimeMs: number;
493
+ checkpoint: string;
494
+ serviceTier: ServiceTierByFamily | null;
495
+ }
496
+
473
497
  export interface ParseSessionResult {
474
498
  stats: MessageStatsInput[];
475
499
  userStats: UserMessageStats[];
@@ -477,11 +501,80 @@ export interface ParseSessionResult {
477
501
  toolCalls: ToolCallStats[];
478
502
  toolResults: ToolResultLink[];
479
503
  newOffset: number;
504
+ parserState?: SessionParserState;
505
+ reset?: boolean;
506
+ }
507
+
508
+ const CHECKPOINT_BYTES = 256;
509
+
510
+ async function readCheckpoint(handle: fs.FileHandle, end: number): Promise<Uint8Array> {
511
+ // Positional reads keep the descriptor at zero for Bun.file(fd)'s subsequent tail read.
512
+ const start = Math.max(0, end - CHECKPOINT_BYTES);
513
+ const bytes = new Uint8Array(end - start);
514
+ let read = 0;
515
+ while (read < bytes.length) {
516
+ const result = await handle.read(bytes, read, bytes.length - read, start + read);
517
+ if (result.bytesRead === 0) break;
518
+ read += result.bytesRead;
519
+ }
520
+ return bytes.subarray(0, read);
521
+ }
522
+
523
+ export function matchesSessionFile(state: SessionParserState, info: nodeFs.Stats): boolean {
524
+ return state.dev === info.dev && state.ino === info.ino && state.birthtimeMs === info.birthtimeMs;
480
525
  }
481
- export async function parseSessionFile(sessionPath: string, fromOffset = 0): Promise<ParseSessionResult> {
526
+
527
+ /** Offset-only callers reconstruct service-tier state once; persisted cursors read only the appended tail. */
528
+ export async function parseSessionFile(
529
+ sessionPath: string,
530
+ fromOffset = 0,
531
+ state?: SessionParserState,
532
+ replay = false,
533
+ ): Promise<ParseSessionResult> {
482
534
  let bytes: Uint8Array;
535
+ let start = fromOffset;
536
+ let reset = false;
537
+ let currentServiceTier: ServiceTierByFamily | undefined;
538
+ let info: nodeFs.Stats;
539
+ let checkpoint: string;
540
+ let read: number;
541
+ let entries: SessionEntry[];
483
542
  try {
484
- bytes = await Bun.file(sessionPath).bytes();
543
+ const handle = await fs.open(sessionPath, "r");
544
+ try {
545
+ info = await handle.stat();
546
+ const file = Bun.file(handle.fd);
547
+ let resume = state?.version === 1 && state.offset === fromOffset;
548
+ if (resume && state) {
549
+ reset =
550
+ !matchesSessionFile(state, info) ||
551
+ info.size < state.size ||
552
+ (info.size === state.size && info.mtimeMs !== state.mtimeMs);
553
+ if (!reset) {
554
+ const previous = await readCheckpoint(handle, fromOffset);
555
+ reset = Bun.hash(previous).toString(16) !== state.checkpoint;
556
+ }
557
+ resume = !reset;
558
+ }
559
+ if (fromOffset > info.size) reset = true;
560
+ if (replay) resume = false;
561
+ start = reset || replay ? 0 : Math.max(0, fromOffset);
562
+ const readStart = resume ? start : 0;
563
+ bytes = await file.slice(readStart, info.size).bytes();
564
+ currentServiceTier = resume
565
+ ? (state?.serviceTier ?? undefined)
566
+ : scanLastServiceTier(bytes.subarray(0, start));
567
+ ({ entries, read } = parseSessionEntriesLenient(bytes.subarray(start - readStart)));
568
+ const newOffset = start + read;
569
+ const checkpointStart = Math.max(0, newOffset - CHECKPOINT_BYTES);
570
+ const previous =
571
+ checkpointStart >= readStart
572
+ ? bytes.subarray(checkpointStart - readStart, newOffset - readStart)
573
+ : await readCheckpoint(handle, newOffset);
574
+ checkpoint = Bun.hash(previous).toString(16);
575
+ } finally {
576
+ await handle.close();
577
+ }
485
578
  } catch (err) {
486
579
  if (isEnoent(err))
487
580
  return { stats: [], userStats: [], userLinks: [], toolCalls: [], toolResults: [], newOffset: fromOffset };
@@ -496,13 +589,6 @@ export async function parseSessionFile(sessionPath: string, fromOffset = 0): Pro
496
589
  const toolCalls: ToolCallStats[] = [];
497
590
  const toolResults: ToolResultLink[] = [];
498
591
  const userByEntryId = new Map<string, UserMessageStats>();
499
- const start = Math.max(0, Math.min(fromOffset, bytes.length));
500
- const unprocessed = bytes.subarray(start);
501
- const { entries, read } = parseSessionEntriesLenient(unprocessed);
502
- let currentServiceTier: ServiceTierByFamily | undefined;
503
- if (start > 0) {
504
- currentServiceTier = scanLastServiceTier(bytes.subarray(0, start));
505
- }
506
592
  for (const entry of entries) {
507
593
  if (isServiceTierChange(entry)) {
508
594
  currentServiceTier = coerceServiceTierByFamily(entry.serviceTier);
@@ -552,7 +638,27 @@ export async function parseSessionFile(sessionPath: string, fromOffset = 0): Pro
552
638
  }
553
639
  }
554
640
 
555
- return { stats, userStats, userLinks, toolCalls, toolResults, newOffset: start + read };
641
+ const newOffset = start + read;
642
+ return {
643
+ stats,
644
+ userStats,
645
+ userLinks,
646
+ toolCalls,
647
+ toolResults,
648
+ newOffset,
649
+ reset,
650
+ parserState: {
651
+ version: 1,
652
+ offset: newOffset,
653
+ dev: info.dev,
654
+ ino: info.ino,
655
+ birthtimeMs: info.birthtimeMs,
656
+ size: info.size,
657
+ mtimeMs: info.mtimeMs,
658
+ checkpoint,
659
+ serviceTier: currentServiceTier ?? null,
660
+ },
661
+ };
556
662
  }
557
663
 
558
664
  /**
@@ -26,7 +26,7 @@ export const STATS_DASHBOARD_SECURITY_VERSION = "3";
26
26
  /** IPv4 loopback address shared by the dashboard server and reuse probe. */
27
27
  export const STATS_DASHBOARD_HOSTNAME = "127.0.0.1";
28
28
 
29
- type StatsDashboardProbe = "reusable" | "occupied" | "unreachable";
29
+ type StatsDashboardProbe = "reusable" | "replaceable" | "occupied" | "unreachable";
30
30
 
31
31
  async function probeStatsDashboard(port: number, hostname: string): Promise<StatsDashboardProbe> {
32
32
  const probeHostname = hostname === "0.0.0.0" ? STATS_DASHBOARD_HOSTNAME : hostname === "::" ? "::1" : hostname;
@@ -35,13 +35,22 @@ async function probeStatsDashboard(port: number, hostname: string): Promise<Stat
35
35
  const response = await fetch(`http://${urlHostname}:${port}/api/stats/models`, {
36
36
  signal: AbortSignal.timeout(STATS_PROBE_TIMEOUT_MS),
37
37
  });
38
+ const dashboardVersionHeader = response.headers.get(STATS_DASHBOARD_HEADER);
39
+ const dashboardVersion = dashboardVersionHeader === null ? Number.NaN : Number(dashboardVersionHeader);
40
+ // Never replace a newer dashboard: an older CLI must not downgrade it.
41
+ const replaceable =
42
+ response.status === 200 &&
43
+ Number.isSafeInteger(dashboardVersion) &&
44
+ dashboardVersion > 0 &&
45
+ dashboardVersion <= Number(STATS_DASHBOARD_SECURITY_VERSION);
38
46
  const reusable =
39
47
  response.status === 200 &&
40
- response.headers.get(STATS_DASHBOARD_HEADER) === STATS_DASHBOARD_SECURITY_VERSION &&
48
+ dashboardVersionHeader === STATS_DASHBOARD_SECURITY_VERSION &&
41
49
  response.headers.get(STATS_DASHBOARD_HOSTNAME_HEADER) === hostname &&
42
50
  !response.headers.has("Access-Control-Allow-Origin");
43
51
  await response.body?.cancel();
44
- return reusable ? "reusable" : "occupied";
52
+ if (reusable) return "reusable";
53
+ return replaceable ? "replaceable" : "occupied";
45
54
  } catch {
46
55
  return "unreachable";
47
56
  }
@@ -218,7 +227,7 @@ async function terminatePortHolder(holder: PortHolder): Promise<void> {
218
227
  await Bun.sleep(PROCESS_EXIT_POLL_MS);
219
228
  }
220
229
 
221
- async function reclaimStatsPort(port: number): Promise<"retry"> {
230
+ async function reclaimStatsPort(port: number, hasDashboardIdentity = false): Promise<"retry"> {
222
231
  const holder = await findPortHolder(port);
223
232
  if (!holder) {
224
233
  throw new Error(`Port ${port} is in use, but the listening process could not be identified.`);
@@ -238,7 +247,7 @@ async function reclaimStatsPort(port: number): Promise<"retry"> {
238
247
  /\/packages\/stats\/src\/index\.ts(?:["'\s]|$)/.test(normalizedCommand) ||
239
248
  (normalizedImage === "omp" && /(?:^|\s)stats(?:\s|$)/.test(normalizedCommand)) ||
240
249
  /(?:^|\/)omp(?:\.exe)?["'\s]+stats(?:["'\s]|$)/.test(normalizedCommand);
241
- if (!STATS_RUNTIME_IMAGES[normalizedImage] || !hasStatsIdentity) {
250
+ if (!STATS_RUNTIME_IMAGES[normalizedImage] || (!hasStatsIdentity && !hasDashboardIdentity)) {
242
251
  throw new Error(
243
252
  `Port ${port} is in use by ${holder.image} (PID ${holder.pid}), which is not identifiable as an omp stats dashboard; refusing to stop it.`,
244
253
  );
@@ -257,12 +266,14 @@ export async function prepareStatsPort(port: number, hostname = STATS_DASHBOARD_
257
266
  if (port === 0) return "retry";
258
267
  const probe = await probeStatsDashboard(port, hostname);
259
268
  if (probe === "reusable") return "reuse";
269
+ if (probe === "replaceable") return reclaimStatsPort(port, true);
260
270
  if (probe === "occupied") return reclaimStatsPort(port);
261
271
  return "retry";
262
272
  }
263
273
 
264
274
  /** Reuse or reclaim a listener found after the server bind reports EADDRINUSE. */
265
275
  export async function recoverStatsPort(port: number, hostname = STATS_DASHBOARD_HOSTNAME): Promise<"retry" | "reuse"> {
266
- if ((await probeStatsDashboard(port, hostname)) === "reusable") return "reuse";
267
- return reclaimStatsPort(port);
276
+ const probe = await probeStatsDashboard(port, hostname);
277
+ if (probe === "reusable") return "reuse";
278
+ return reclaimStatsPort(port, probe === "replaceable");
268
279
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Stateless parse worker for `syncAllSessions`. The main thread owns the
3
- * SQLite handle; workers receive `{ sessionFile, fromOffset }`, run
3
+ * SQLite handle; workers receive a session path, offset, and parser state, run
4
4
  * `parseSessionFile` (which is pure I/O + CPU, no DB), and post the
5
5
  * structured-clone-safe result back. One in-flight request per worker so
6
6
  * the main thread can fan jobs out 1:1 with the pool size.
@@ -11,9 +11,11 @@
11
11
  * for issue #1011 / PR #1027, where the worker silently failed to load).
12
12
  */
13
13
 
14
- import { type ParseSessionResult, parseSessionFile } from "./parser";
14
+ import { type ParseSessionResult, parseSessionFile, type SessionParserState } from "./parser";
15
15
 
16
- export type SyncWorkerRequest = { kind?: "parse"; sessionFile: string; fromOffset: number } | { kind: "ping" };
16
+ export type SyncWorkerRequest =
17
+ | { kind?: "parse"; sessionFile: string; fromOffset: number; parserState?: SessionParserState; replay?: boolean }
18
+ | { kind: "ping" };
17
19
 
18
20
  export type SyncWorkerResponse =
19
21
  | { ok: true; kind?: "parse"; result: ParseSessionResult }
@@ -31,7 +33,12 @@ self.onmessage = async event => {
31
33
  self.postMessage({ ok: true, kind: "pong" } satisfies SyncWorkerResponse);
32
34
  return;
33
35
  }
34
- const result = await parseSessionFile(request.sessionFile, request.fromOffset);
36
+ const result = await parseSessionFile(
37
+ request.sessionFile,
38
+ request.fromOffset,
39
+ request.parserState,
40
+ request.replay,
41
+ );
35
42
  self.postMessage({ ok: true, result } satisfies SyncWorkerResponse);
36
43
  } catch (err) {
37
44
  const error = err instanceof Error ? (err.stack ?? err.message) : String(err);
package/src/trace.ts CHANGED
@@ -854,22 +854,54 @@ export async function getTraceEntry(fileParam: string, entryId: string): Promise
854
854
  // ---------------------------------------------------------------------------
855
855
  // Session list
856
856
 
857
- const titleCache = new Map<string, { mtimeMs: number; title: string | null }>();
857
+ interface SessionMetadata {
858
+ title: string | null;
859
+ cwd: string | null;
860
+ }
861
+
862
+ const metadataCache = new Map<string, SessionMetadata & { mtimeMs: number }>();
858
863
 
859
- /** Read the session title from the fixed title slot or the header line. */
860
- async function readSessionTitle(file: string): Promise<string | null> {
864
+ /** Read enough decompressed transcript bytes to cover the title slot and session header. */
865
+ async function readSessionMetadataHead(file: string): Promise<string> {
866
+ if (!file.endsWith(".gz")) return Bun.file(file).slice(0, TITLE_SCAN_BYTES).text();
867
+
868
+ const reader = Bun.file(file).stream().pipeThrough(new DecompressionStream("gzip")).getReader();
869
+ const decoder = new TextDecoder();
870
+ let remaining = TITLE_SCAN_BYTES;
871
+ let head = "";
872
+ try {
873
+ while (remaining > 0) {
874
+ const { done, value } = await reader.read();
875
+ if (done) {
876
+ head += decoder.decode();
877
+ break;
878
+ }
879
+ const chunk = value.subarray(0, remaining);
880
+ remaining -= chunk.byteLength;
881
+ head += decoder.decode(chunk, { stream: remaining > 0 && chunk.byteLength === value.byteLength });
882
+ if (chunk.byteLength < value.byteLength) break;
883
+ }
884
+ } finally {
885
+ await reader.cancel();
886
+ }
887
+ return head;
888
+ }
889
+
890
+ /** Read list metadata from the fixed title slot and session header. */
891
+ async function readSessionMetadata(file: string): Promise<SessionMetadata> {
861
892
  let mtimeMs: number;
862
893
  try {
863
894
  mtimeMs = (await fs.stat(file)).mtimeMs;
864
895
  } catch {
865
- return null;
896
+ return { title: null, cwd: null };
866
897
  }
867
- const cached = titleCache.get(file);
868
- if (cached && cached.mtimeMs === mtimeMs) return cached.title;
898
+ const cached = metadataCache.get(file);
899
+ if (cached && cached.mtimeMs === mtimeMs) return cached;
869
900
 
870
901
  let title: string | null = null;
902
+ let cwd: string | null = null;
871
903
  try {
872
- const head = await Bun.file(file).slice(0, TITLE_SCAN_BYTES).text();
904
+ const head = await readSessionMetadataHead(file);
873
905
  for (const line of head.split("\n")) {
874
906
  if (!line.trim()) continue;
875
907
  let parsed: EntryView;
@@ -881,18 +913,20 @@ async function readSessionTitle(file: string): Promise<string | null> {
881
913
  }
882
914
  if (parsed.type === "title" && typeof parsed.title === "string") {
883
915
  title = parsed.title;
884
- break;
916
+ continue;
885
917
  }
886
918
  if (parsed.type === "session") {
887
- if (typeof parsed.title === "string") title = parsed.title;
919
+ if (title === null && typeof parsed.title === "string") title = parsed.title;
920
+ if (typeof parsed.cwd === "string") cwd = parsed.cwd;
888
921
  break;
889
922
  }
890
923
  }
891
924
  } catch {
892
- // Unreadable head (gc'd, gz, permission) → keep the row, title null.
925
+ // Unreadable head (gc'd, gz, permission) → keep the row with path-derived metadata.
893
926
  }
894
- titleCache.set(file, { mtimeMs, title });
895
- return title;
927
+ const metadata = { mtimeMs, title, cwd };
928
+ metadataCache.set(file, metadata);
929
+ return metadata;
896
930
  }
897
931
 
898
932
  interface SummaryFold extends SessionSummary {
@@ -1027,7 +1061,9 @@ export async function listSessionSummaries(limit = 100, q?: string): Promise<Ses
1027
1061
  const folds = [...byRoot.values()].sort((a, b) => b.endedAt - a.endedAt).slice(0, LIST_FOLD_LIMIT);
1028
1062
  await Promise.all(
1029
1063
  folds.map(async fold => {
1030
- fold.title = await readSessionTitle(fold.file);
1064
+ const metadata = await readSessionMetadata(fold.file);
1065
+ fold.title = metadata.title;
1066
+ fold.folder = metadata.cwd ?? extractFolderFromPath(fold.file);
1031
1067
  fold.models = [...fold.modelSet].sort();
1032
1068
  }),
1033
1069
  );