claude-session-viewer 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dev.js +20 -1
- package/dist/client/assets/index-Csvqk3mE.js +70 -0
- package/dist/client/assets/index-DOk7moPK.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/claude/projects/repository.js +1 -1
- package/dist/server/claude/projects/service.js +4 -4
- package/dist/server/claude/sessions/service.js +9 -5
- package/dist/server/routes/sessionWindows.js +127 -0
- package/dist/server/routes/sessions.js +5 -4
- package/dist/server/routes/statistics.js +34 -310
- package/dist/server/statistics/aggregator.js +290 -0
- package/dist/server/statistics/service.js +226 -0
- package/dist/server/statistics/tokenStats.js +66 -0
- package/dist/server/statistics/tokenUsage.js +66 -0
- package/dist/server/statistics/utils.js +33 -0
- package/dist/server/utils/sessionWindows.js +159 -0
- package/package.json +2 -2
- package/dist/client/assets/index-DvK33tag.css +0 -1
- package/dist/client/assets/index-KEbXAXOS.js +0 -69
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.left-1\/2{left:50%}.right-0{right:0}.top-0{top:0}.top-full{top:100%}.z-10{z-index:10}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.-mt-px{margin-top:-1px}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-96{height:24rem}.h-full{height:100%}.h-screen{height:100vh}.w-1{width:.25rem}.w-1\/2{width:50%}.w-1\/3{width:33.333333%}.w-12{width:3rem}.w-16{width:4rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-4{width:1rem}.w-4\/5{width:80%}.w-5{width:1.25rem}.w-5\/6{width:83.333333%}.w-64{width:16rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-col-resize{cursor:col-resize}.cursor-pointer{cursor:pointer}.resize{resize:both}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-blue-400{--tw-border-opacity: 1;border-color:rgb(96 165 250 / var(--tw-border-opacity, 1))}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-blue-500\/20{border-color:#3b82f633}.border-cyan-500\/20{border-color:#06b6d433}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-green-500\/20{border-color:#22c55e33}.border-orange-500\/20{border-color:#f9731633}.border-pink-500\/20{border-color:#ec489933}.border-purple-500\/20{border-color:#a855f733}.border-transparent{border-color:transparent}.border-t-gray-900{--tw-border-opacity: 1;border-top-color:rgb(17 24 39 / var(--tw-border-opacity, 1))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.bg-blue-900\/50{background-color:#1e3a8a80}.bg-cyan-500{--tw-bg-opacity: 1;background-color:rgb(6 182 212 / var(--tw-bg-opacity, 1))}.bg-cyan-500\/10{background-color:#06b6d41a}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-800\/30{background-color:#1f29374d}.bg-gray-800\/50{background-color:#1f293780}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-900\/50{background-color:#11182780}.bg-gray-900\/70{background-color:#111827b3}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.bg-green-900\/30{background-color:#14532d4d}.bg-green-900\/50{background-color:#14532d80}.bg-orange-500{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity, 1))}.bg-orange-500\/10{background-color:#f973161a}.bg-pink-500{--tw-bg-opacity: 1;background-color:rgb(236 72 153 / var(--tw-bg-opacity, 1))}.bg-pink-500\/10{background-color:#ec48991a}.bg-purple-500{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity, 1))}.bg-purple-500\/10{background-color:#a855f71a}.bg-purple-700{--tw-bg-opacity: 1;background-color:rgb(126 34 206 / var(--tw-bg-opacity, 1))}.bg-purple-900\/50{background-color:#581c8780}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.bg-yellow-900\/30{background-color:#713f124d}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from: #3b82f6 var(--tw-gradient-from-position);--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-green-500{--tw-gradient-to: #22c55e var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pl-14{padding-left:3.5rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.leading-tight{line-height:1.25}.text-blue-200{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-cyan-400{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-200{--tw-text-opacity: 1;color:rgb(187 247 208 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-purple-100{--tw-text-opacity: 1;color:rgb(243 232 255 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.\@container{container-type:inline-size}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}#root{width:100%;min-height:100vh}*{scrollbar-width:thin;scrollbar-color:#4B5563 #1F2937}*::-webkit-scrollbar{width:8px;height:8px}*::-webkit-scrollbar-track{background:#1f2937}*::-webkit-scrollbar-thumb{background-color:#4b5563;border-radius:4px}*::-webkit-scrollbar-thumb:hover{background-color:#6b7280}.last\:border-b-0:last-child{border-bottom-width:0px}.last\:pb-0:last-child{padding-bottom:0}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-600:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700\/50:hover{background-color:#37415180}.hover\:bg-gray-800:hover{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-600:hover{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity, 1))}.hover\:text-gray-200:hover{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.group:hover .group-hover\:bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}@container (min-width: 300px){.\@\[300px\]\:inline{display:inline}.\@\[300px\]\:hidden{display:none}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
package/dist/client/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Claude Session Viewer</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Csvqk3mE.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DOk7moPK.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
|
@@ -30,7 +30,7 @@ export function getProjectPath(projectsDir, projectName) {
|
|
|
30
30
|
* Remove user's home directory prefix from project directory name
|
|
31
31
|
* e.g., "-Users-hanyeol-Projects-foo" → "Projects-foo"
|
|
32
32
|
*/
|
|
33
|
-
export function
|
|
33
|
+
export function getProjectName(projectDirName) {
|
|
34
34
|
const userHomePath = homedir().split('/').filter(Boolean).join('-');
|
|
35
35
|
const prefix = `-${userHomePath}-`;
|
|
36
36
|
if (projectDirName.startsWith(prefix)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { stat } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { listProjects,
|
|
3
|
+
import { listProjects, getProjectName } from './repository.js';
|
|
4
4
|
import { getProjectSessions, sortSessionsByTimestamp } from '../sessions/service.js';
|
|
5
5
|
/**
|
|
6
6
|
* Project service
|
|
@@ -25,10 +25,10 @@ export async function getAllProjectsWithSessions(projectsDir) {
|
|
|
25
25
|
const sessions = await getProjectSessions(projectPath);
|
|
26
26
|
if (sessions.length > 0) {
|
|
27
27
|
sortSessionsByTimestamp(sessions);
|
|
28
|
-
const
|
|
28
|
+
const name = getProjectName(project);
|
|
29
29
|
projectGroups.push({
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
id: project,
|
|
31
|
+
name,
|
|
32
32
|
sessionCount: sessions.length,
|
|
33
33
|
lastActivity: sessions[0].timestamp,
|
|
34
34
|
sessions
|
|
@@ -3,7 +3,7 @@ import { listSessionFiles, readSessionFile } from './repository.js';
|
|
|
3
3
|
import { shouldSkipSession, isAgentSession, isEmptyFile } from './filters.js';
|
|
4
4
|
import { extractSessionTitle } from './title.js';
|
|
5
5
|
import { collectAgentDescriptions, attachAgentSessionsToParent } from './agents.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getProjectName } from '../projects/repository.js';
|
|
7
7
|
/**
|
|
8
8
|
* Session service
|
|
9
9
|
* Orchestrates session loading, filtering, and organization
|
|
@@ -21,7 +21,8 @@ export async function getProjectSessions(projectPath) {
|
|
|
21
21
|
const sessionFiles = await listSessionFiles(projectPath);
|
|
22
22
|
const allSessions = [];
|
|
23
23
|
const agentSessionsMap = new Map();
|
|
24
|
-
const
|
|
24
|
+
const projectId = projectPath.split('/').pop() || 'unknown';
|
|
25
|
+
const projectName = getProjectName(projectId);
|
|
25
26
|
// First pass: load all sessions
|
|
26
27
|
for (const { filename, size, path } of sessionFiles) {
|
|
27
28
|
if (isEmptyFile(size))
|
|
@@ -33,7 +34,8 @@ export async function getProjectSessions(projectPath) {
|
|
|
33
34
|
continue;
|
|
34
35
|
const session = {
|
|
35
36
|
id: sessionId,
|
|
36
|
-
|
|
37
|
+
projectId,
|
|
38
|
+
projectName,
|
|
37
39
|
timestamp,
|
|
38
40
|
messages,
|
|
39
41
|
messageCount: messages.length,
|
|
@@ -61,15 +63,17 @@ export async function getProjectSessions(projectPath) {
|
|
|
61
63
|
/**
|
|
62
64
|
* Load agent sessions from files
|
|
63
65
|
*/
|
|
64
|
-
export async function loadAgentSessionsFromFiles(projectPath,
|
|
66
|
+
export async function loadAgentSessionsFromFiles(projectPath, projectId, agentDescriptions) {
|
|
65
67
|
const agentSessions = [];
|
|
68
|
+
const projectName = getProjectName(projectId);
|
|
66
69
|
for (const [agentSessionId, description] of agentDescriptions) {
|
|
67
70
|
const agentFile = join(projectPath, `${agentSessionId}.jsonl`);
|
|
68
71
|
try {
|
|
69
72
|
const { messages, timestamp } = await readSessionFile(agentFile);
|
|
70
73
|
agentSessions.push({
|
|
71
74
|
id: agentSessionId,
|
|
72
|
-
|
|
75
|
+
projectId,
|
|
76
|
+
projectName,
|
|
73
77
|
timestamp,
|
|
74
78
|
messages,
|
|
75
79
|
messageCount: messages.length,
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readdir, stat } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { CLAUDE_DIR } from '../constants.js';
|
|
4
|
+
import { parseJsonl } from '../utils/jsonl.js';
|
|
5
|
+
import { isAgentSession, isEmptyFile, shouldSkipSession } from '../claude/sessions/filters.js';
|
|
6
|
+
import { calculateSessionWindows, getActiveWindow, loadPlanConfig } from '../utils/sessionWindows.js';
|
|
7
|
+
import { PLAN_LIMITS } from '../claude/config.js';
|
|
8
|
+
/**
|
|
9
|
+
* Session window routes
|
|
10
|
+
*/
|
|
11
|
+
export async function registerSessionWindowRoutes(server) {
|
|
12
|
+
/**
|
|
13
|
+
* GET /api/session-windows
|
|
14
|
+
* Returns all 5-hour session windows with token usage
|
|
15
|
+
*/
|
|
16
|
+
server.get('/api/session-windows', async (request, reply) => {
|
|
17
|
+
try {
|
|
18
|
+
const projectsDir = join(CLAUDE_DIR, 'projects');
|
|
19
|
+
const projects = await readdir(projectsDir);
|
|
20
|
+
// Parse days parameter
|
|
21
|
+
const daysParam = request.query.days || '8'; // Default 8 days for P90 calculation
|
|
22
|
+
const cutoffDate = new Date();
|
|
23
|
+
if (daysParam !== 'all') {
|
|
24
|
+
const days = parseInt(daysParam, 10);
|
|
25
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
26
|
+
cutoffDate.setHours(0, 0, 0, 0);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
cutoffDate.setFullYear(2000, 0, 1);
|
|
30
|
+
}
|
|
31
|
+
// Collect all session data
|
|
32
|
+
const sessionsData = [];
|
|
33
|
+
for (const project of projects) {
|
|
34
|
+
const projectPath = join(projectsDir, project);
|
|
35
|
+
const projectStat = await stat(projectPath);
|
|
36
|
+
if (!projectStat.isDirectory())
|
|
37
|
+
continue;
|
|
38
|
+
const files = await readdir(projectPath);
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (!file.endsWith('.jsonl'))
|
|
41
|
+
continue;
|
|
42
|
+
const filePath = join(projectPath, file);
|
|
43
|
+
const fileStat = await stat(filePath);
|
|
44
|
+
if (isEmptyFile(fileStat.size))
|
|
45
|
+
continue;
|
|
46
|
+
const sessionId = file.replace('.jsonl', '');
|
|
47
|
+
if (isAgentSession(sessionId))
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
const messages = await parseJsonl(filePath);
|
|
51
|
+
if (shouldSkipSession(messages))
|
|
52
|
+
continue;
|
|
53
|
+
// Filter messages by date
|
|
54
|
+
const filteredMessages = messages.filter((msg) => {
|
|
55
|
+
if (!msg.timestamp)
|
|
56
|
+
return false;
|
|
57
|
+
const msgDate = new Date(msg.timestamp);
|
|
58
|
+
return msgDate >= cutoffDate;
|
|
59
|
+
});
|
|
60
|
+
if (filteredMessages.length > 0) {
|
|
61
|
+
sessionsData.push({
|
|
62
|
+
sessionId,
|
|
63
|
+
messages: filteredMessages
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error(`Error processing ${file}:`, error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Calculate initial windows with default plan for detection
|
|
73
|
+
const initialWindows = calculateSessionWindows(sessionsData, {
|
|
74
|
+
name: 'Custom',
|
|
75
|
+
limits: PLAN_LIMITS.Custom,
|
|
76
|
+
autoDetected: false
|
|
77
|
+
});
|
|
78
|
+
// Auto-detect plan based on windows
|
|
79
|
+
const planConfig = loadPlanConfig(initialWindows);
|
|
80
|
+
// Recalculate windows with detected plan
|
|
81
|
+
const windows = calculateSessionWindows(sessionsData, planConfig);
|
|
82
|
+
// Get active window
|
|
83
|
+
const activeWindow = getActiveWindow(windows);
|
|
84
|
+
const response = {
|
|
85
|
+
windows,
|
|
86
|
+
activeWindow,
|
|
87
|
+
planConfig
|
|
88
|
+
};
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error('Error calculating session windows:', error);
|
|
93
|
+
return reply.code(500).send({ error: 'Internal server error' });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* GET /api/session-windows/active
|
|
98
|
+
* Returns only the currently active session window
|
|
99
|
+
*/
|
|
100
|
+
server.get('/api/session-windows/active', async (request, reply) => {
|
|
101
|
+
try {
|
|
102
|
+
// Reuse the main endpoint logic but return only active window
|
|
103
|
+
const fullResponse = await server.inject({
|
|
104
|
+
method: 'GET',
|
|
105
|
+
url: '/api/session-windows?days=1' // Only check last day for performance
|
|
106
|
+
});
|
|
107
|
+
if (fullResponse.statusCode !== 200) {
|
|
108
|
+
return reply.code(fullResponse.statusCode).send(fullResponse.json());
|
|
109
|
+
}
|
|
110
|
+
const data = fullResponse.json();
|
|
111
|
+
if (!data.activeWindow) {
|
|
112
|
+
return {
|
|
113
|
+
activeWindow: null,
|
|
114
|
+
planConfig: data.planConfig
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
activeWindow: data.activeWindow,
|
|
119
|
+
planConfig: data.planConfig
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.error('Error getting active session window:', error);
|
|
124
|
+
return reply.code(500).send({ error: 'Internal server error' });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -3,7 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import { CLAUDE_DIR } from '../constants.js';
|
|
4
4
|
import { parseJsonl } from '../utils/jsonl.js';
|
|
5
5
|
import { getAllProjectsWithSessions } from '../claude/projects/service.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getProjectName } from '../claude/projects/repository.js';
|
|
7
7
|
import { loadAgentSessionsFromFiles } from '../claude/sessions/service.js';
|
|
8
8
|
import { collectAgentDescriptions, injectAgentIdsIntoMessages, findAgentTitleFromParentMessages } from '../claude/sessions/agents.js';
|
|
9
9
|
import { isAgentSession } from '../claude/sessions/filters.js';
|
|
@@ -37,7 +37,7 @@ export async function registerSessionRoutes(server) {
|
|
|
37
37
|
try {
|
|
38
38
|
const messages = await parseJsonl(sessionFile);
|
|
39
39
|
const fileStat = await stat(sessionFile);
|
|
40
|
-
const projectName =
|
|
40
|
+
const projectName = project;
|
|
41
41
|
let title = extractSessionTitle(messages);
|
|
42
42
|
// For agent sessions, find description from parent
|
|
43
43
|
if (isAgent) {
|
|
@@ -64,14 +64,15 @@ export async function registerSessionRoutes(server) {
|
|
|
64
64
|
if (!isAgent) {
|
|
65
65
|
const agentDescriptions = collectAgentDescriptions(messages);
|
|
66
66
|
if (agentDescriptions.size > 0) {
|
|
67
|
-
agentSessions = await loadAgentSessionsFromFiles(projectPath,
|
|
67
|
+
agentSessions = await loadAgentSessionsFromFiles(projectPath, project, agentDescriptions);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
const messagesWithAgentIds = injectAgentIdsIntoMessages(messages);
|
|
71
71
|
return {
|
|
72
72
|
session: {
|
|
73
73
|
id,
|
|
74
|
-
|
|
74
|
+
projectId: projectName,
|
|
75
|
+
projectName: getProjectName(projectName),
|
|
75
76
|
timestamp: fileStat.mtime.toISOString(),
|
|
76
77
|
messages: messagesWithAgentIds,
|
|
77
78
|
messageCount: messages.length,
|