loki-mode 6.35.1 → 6.36.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/README.md +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/web-app/dist/assets/index-Cm-odtQC.js +66 -0
- package/web-app/dist/assets/index-SenDDIyx.css +1 -0
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +276 -98
- package/web-app/dist/assets/index-DW1e50zX.css +0 -1
- package/web-app/dist/assets/index-HYTmiwkW.js +0 -66
|
@@ -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:Inter,system-ui,-apple-system,sans-serif;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:JetBrains Mono,Fira Code,Cascadia Code,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}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.terminal-scroll::-webkit-scrollbar{width:6px}.terminal-scroll::-webkit-scrollbar-track{background:transparent}.terminal-scroll::-webkit-scrollbar-thumb{background:#3d52a033;border-radius:3px}.terminal-scroll::-webkit-scrollbar-thumb:hover{background:#3d52a059}.glass{background:#fff9;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,.3);border-radius:16px;box-shadow:0 8px 32px #3d52a014}.glass-subtle{background:#fff6;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,.2);border-radius:12px;box-shadow:0 4px 16px #3d52a00f}.pattern-circles{position:absolute;top:0;right:0;bottom:0;left:0;opacity:.04;background-image:radial-gradient(circle at 20% 30%,transparent 30%,#6C63FF 30.5%,transparent 31%),radial-gradient(circle at 80% 70%,transparent 25%,#6C63FF 25.5%,transparent 26%),radial-gradient(circle at 50% 50%,transparent 40%,#6C63FF 40.5%,transparent 41%);pointer-events:none}@keyframes phase-pulse{0%,to{opacity:1}50%{opacity:.5}}.phase-active{animation:phase-pulse 2s ease-in-out infinite}@keyframes cursor-blink{0%,to{opacity:1}50%{opacity:0}}.terminal-cursor:after{content:"";display:inline-block;width:8px;height:16px;background:#7091e6;animation:cursor-blink 1s step-end infinite;margin-left:2px;vertical-align:text-bottom}.\!visible{visibility:visible!important}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.right-0{right:0}.top-0{top:0}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.col-span-3{grid-column:span 3 / span 3}.col-span-4{grid-column:span 4 / span 4}.col-span-5{grid-column:span 5 / span 5}.col-span-6{grid-column:span 6 / span 6}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.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}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.h-1\.5{height:.375rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-28{height:7rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-full{height:100%}.max-h-32{max-height:8rem}.max-h-40{max-height:10rem}.max-h-64{max-height:16rem}.max-h-\[400px\]{max-height:400px}.min-h-0{min-height:0px}.min-h-\[280px\]{min-height:280px}.min-h-\[300px\]{min-height:300px}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-1\/2{width:50%}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-28{width:7rem}.w-3{width:.75rem}.w-4{width:1rem}.w-56{width:14rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.max-w-3xl{max-width:48rem}.max-w-\[1920px\]{max-width:1920px}.max-w-\[220px\]{max-width:220px}.max-w-lg{max-width:32rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,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}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0px}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.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))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-accent-product{--tw-border-opacity: 1;border-color:rgb(108 99 255 / var(--tw-border-opacity, 1))}.border-accent-product\/20{border-color:#6c63ff33}.border-accent-product\/30{border-color:#6c63ff4d}.border-danger\/20{border-color:#c45b5b33}.border-primary-wash\/30{border-color:#8697c44d}.border-primary\/20{border-color:#3d52a033}.border-slate\/20{border-color:#4f5d7533}.border-success\/20{border-color:#7fb06933}.border-surface\/50{border-color:#adbbda80}.border-warning\/20{border-color:#d4a03c33}.border-warning\/30{border-color:#d4a03c4d}.border-warning\/40{border-color:#d4a03c66}.border-white\/10{border-color:#ffffff1a}.border-white\/20{border-color:#fff3}.border-white\/30{border-color:#ffffff4d}.border-t-transparent{border-top-color:transparent}.bg-accent-product{--tw-bg-opacity: 1;background-color:rgb(108 99 255 / var(--tw-bg-opacity, 1))}.bg-accent-product\/10{background-color:#6c63ff1a}.bg-background{--tw-bg-opacity: 1;background-color:rgb(237 232 245 / var(--tw-bg-opacity, 1))}.bg-black\/30{background-color:#0000004d}.bg-black\/5{background-color:#0000000d}.bg-charcoal\/10{background-color:#2d31421a}.bg-charcoal\/5{background-color:#2d31420d}.bg-charcoal\/\[0\.03\]{background-color:#2d314208}.bg-danger{--tw-bg-opacity: 1;background-color:rgb(196 91 91 / var(--tw-bg-opacity, 1))}.bg-danger\/10{background-color:#c45b5b1a}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(61 82 160 / var(--tw-bg-opacity, 1))}.bg-primary-light{--tw-bg-opacity: 1;background-color:rgb(112 145 230 / var(--tw-bg-opacity, 1))}.bg-primary-wash\/20{background-color:#8697c433}.bg-primary\/10{background-color:#3d52a01a}.bg-slate{--tw-bg-opacity: 1;background-color:rgb(79 93 117 / var(--tw-bg-opacity, 1))}.bg-slate\/10{background-color:#4f5d751a}.bg-slate\/30{background-color:#4f5d754d}.bg-slate\/40{background-color:#4f5d7566}.bg-success{--tw-bg-opacity: 1;background-color:rgb(127 176 105 / var(--tw-bg-opacity, 1))}.bg-success\/10{background-color:#7fb0691a}.bg-surface{--tw-bg-opacity: 1;background-color:rgb(173 187 218 / var(--tw-bg-opacity, 1))}.bg-surface\/30{background-color:#adbbda4d}.bg-surface\/50{background-color:#adbbda80}.bg-warning{--tw-bg-opacity: 1;background-color:rgb(212 160 60 / var(--tw-bg-opacity, 1))}.bg-warning\/10{background-color:#d4a03c1a}.bg-warning\/5{background-color:#d4a03c0d}.bg-white\/30{background-color:#ffffff4d}.bg-white\/40{background-color:#fff6}.fill-charcoal{fill:#2d3142}.fill-primary{fill:#3d52a0}.p-0{padding:0}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2\.5{padding-bottom:.625rem}.pr-2{padding-right:.5rem}.pt-0{padding-top:0}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,Fira Code,Cascadia Code,monospace}.font-sans{font-family:Inter,system-ui,-apple-system,sans-serif}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.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}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-relaxed{line-height:1.625}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-accent-product{--tw-text-opacity: 1;color:rgb(108 99 255 / var(--tw-text-opacity, 1))}.text-charcoal{--tw-text-opacity: 1;color:rgb(45 49 66 / var(--tw-text-opacity, 1))}.text-charcoal\/70{color:#2d3142b3}.text-charcoal\/80{color:#2d3142cc}.text-danger{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.text-primary{--tw-text-opacity: 1;color:rgb(61 82 160 / var(--tw-text-opacity, 1))}.text-primary-light{--tw-text-opacity: 1;color:rgb(112 145 230 / var(--tw-text-opacity, 1))}.text-primary-wash{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.text-slate{--tw-text-opacity: 1;color:rgb(79 93 117 / var(--tw-text-opacity, 1))}.text-slate\/40{color:#4f5d7566}.text-slate\/50{color:#4f5d7580}.text-slate\/60{color:#4f5d7599}.text-success{--tw-text-opacity: 1;color:rgb(127 176 105 / var(--tw-text-opacity, 1))}.text-warning{--tw-text-opacity: 1;color:rgb(212 160 60 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-glass{--tw-shadow: 0 8px 32px rgba(61,82,160,.08);--tw-shadow-colored: 0 8px 32px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-glass-subtle{--tw-shadow: 0 4px 16px rgba(61,82,160,.06);--tw-shadow-colored: 0 4px 16px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 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)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.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}.duration-200{transition-duration:.2s}.duration-500{transition-duration:.5s}.placeholder\:text-primary-wash::-moz-placeholder{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.placeholder\:text-primary-wash::placeholder{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.placeholder\:text-primary-wash\/70::-moz-placeholder{color:#8697c4b3}.placeholder\:text-primary-wash\/70::placeholder{color:#8697c4b3}.hover\:bg-accent-product\/20:hover{background-color:#6c63ff33}.hover\:bg-accent-product\/5:hover{background-color:#6c63ff0d}.hover\:bg-accent-product\/90:hover{background-color:#6c63ffe6}.hover\:bg-danger\/20:hover{background-color:#c45b5b33}.hover\:bg-primary\/5:hover{background-color:#3d52a00d}.hover\:bg-warning\/10:hover{background-color:#d4a03c1a}.hover\:bg-white\/20:hover{background-color:#fff3}.hover\:bg-white\/30:hover{background-color:#ffffff4d}.hover\:bg-white\/40:hover{background-color:#fff6}.hover\:bg-white\/5:hover{background-color:#ffffff0d}.hover\:text-charcoal:hover{--tw-text-opacity: 1;color:rgb(45 49 66 / var(--tw-text-opacity, 1))}.hover\:text-primary-light:hover{--tw-text-opacity: 1;color:rgb(112 145 230 / var(--tw-text-opacity, 1))}.focus\:border-accent-product\/30:focus{border-color:#6c63ff4d}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-accent-product\/20:focus{--tw-ring-color: rgb(108 99 255 / .2)}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-accent-product{--tw-text-opacity: 1;color:rgb(108 99 255 / var(--tw-text-opacity, 1))}
|
package/web-app/dist/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-Cm-odtQC.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-SenDDIyx.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body class="bg-background text-charcoal font-sans antialiased">
|
|
15
15
|
<div id="root"></div>
|
package/web-app/server.py
CHANGED
|
@@ -69,6 +69,24 @@ class SessionState:
|
|
|
69
69
|
self.log_lines: list[str] = []
|
|
70
70
|
self.ws_clients: set[WebSocket] = set()
|
|
71
71
|
self._reader_task: Optional[asyncio.Task] = None
|
|
72
|
+
self._lock = asyncio.Lock()
|
|
73
|
+
|
|
74
|
+
async def cleanup(self) -> None:
|
|
75
|
+
"""Cancel reader task and close process pipes."""
|
|
76
|
+
if self._reader_task and not self._reader_task.done():
|
|
77
|
+
self._reader_task.cancel()
|
|
78
|
+
try:
|
|
79
|
+
await asyncio.wait_for(self._reader_task, timeout=3)
|
|
80
|
+
except (asyncio.CancelledError, asyncio.TimeoutError, Exception):
|
|
81
|
+
pass
|
|
82
|
+
self._reader_task = None
|
|
83
|
+
|
|
84
|
+
if self.process:
|
|
85
|
+
try:
|
|
86
|
+
if self.process.stdout:
|
|
87
|
+
self.process.stdout.close()
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
72
90
|
|
|
73
91
|
def reset(self) -> None:
|
|
74
92
|
self.process = None
|
|
@@ -99,6 +117,9 @@ class StopResponse(BaseModel):
|
|
|
99
117
|
message: str
|
|
100
118
|
|
|
101
119
|
|
|
120
|
+
_MAX_PRD_BYTES = 1_048_576 # 1 MB
|
|
121
|
+
|
|
122
|
+
|
|
102
123
|
class PlanRequest(BaseModel):
|
|
103
124
|
prd: str
|
|
104
125
|
provider: str = "claude"
|
|
@@ -143,7 +164,7 @@ async def _broadcast(msg: dict) -> None:
|
|
|
143
164
|
"""Send a JSON message to all connected WebSocket clients."""
|
|
144
165
|
data = json.dumps(msg)
|
|
145
166
|
dead: list[WebSocket] = []
|
|
146
|
-
for ws in session.ws_clients:
|
|
167
|
+
for ws in list(session.ws_clients):
|
|
147
168
|
try:
|
|
148
169
|
await ws.send_text(data)
|
|
149
170
|
except Exception:
|
|
@@ -158,7 +179,7 @@ async def _read_process_output() -> None:
|
|
|
158
179
|
if proc is None or proc.stdout is None:
|
|
159
180
|
return
|
|
160
181
|
|
|
161
|
-
loop = asyncio.
|
|
182
|
+
loop = asyncio.get_running_loop()
|
|
162
183
|
|
|
163
184
|
try:
|
|
164
185
|
while session.running and proc.poll() is None:
|
|
@@ -223,72 +244,78 @@ async def start_session(req: StartRequest) -> JSONResponse:
|
|
|
223
244
|
"""Start a new loki session with the given PRD."""
|
|
224
245
|
if len(req.prd.encode()) > _MAX_PRD_BYTES:
|
|
225
246
|
return JSONResponse(status_code=400, content={"error": "PRD exceeds 1 MB limit"})
|
|
226
|
-
if session.running:
|
|
227
|
-
return JSONResponse(
|
|
228
|
-
status_code=409,
|
|
229
|
-
content={"error": "A session is already running. Stop it first."},
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
# Determine project directory
|
|
233
|
-
project_dir = req.projectDir
|
|
234
|
-
if not project_dir:
|
|
235
|
-
project_dir = os.path.join(Path.home(), "purple-lab-projects", f"project-{int(time.time())}")
|
|
236
|
-
os.makedirs(project_dir, exist_ok=True)
|
|
237
|
-
|
|
238
|
-
# Write PRD to a temp file in the project dir
|
|
239
|
-
prd_path = os.path.join(project_dir, "PRD.md")
|
|
240
|
-
with open(prd_path, "w") as f:
|
|
241
|
-
f.write(req.prd)
|
|
242
247
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
"
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
248
|
+
async with session._lock:
|
|
249
|
+
if session.running:
|
|
250
|
+
return JSONResponse(
|
|
251
|
+
status_code=409,
|
|
252
|
+
content={"error": "A session is already running. Stop it first."},
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Clean up any stale reader task from previous session
|
|
256
|
+
await session.cleanup()
|
|
257
|
+
|
|
258
|
+
# Determine project directory
|
|
259
|
+
project_dir = req.projectDir
|
|
260
|
+
if not project_dir:
|
|
261
|
+
project_dir = os.path.join(Path.home(), "purple-lab-projects", f"project-{int(time.time())}")
|
|
262
|
+
os.makedirs(project_dir, exist_ok=True)
|
|
263
|
+
|
|
264
|
+
# Write PRD to a temp file in the project dir
|
|
265
|
+
prd_path = os.path.join(project_dir, "PRD.md")
|
|
266
|
+
with open(prd_path, "w") as f:
|
|
267
|
+
f.write(req.prd)
|
|
268
|
+
|
|
269
|
+
# Build the loki start command
|
|
270
|
+
if req.mode == "quick":
|
|
271
|
+
# Extract first non-blank line as the task description
|
|
272
|
+
first_line = next((l.strip() for l in req.prd.splitlines() if l.strip()), req.prd[:200])
|
|
273
|
+
cmd = [
|
|
274
|
+
str(LOKI_CLI),
|
|
275
|
+
"quick",
|
|
276
|
+
first_line,
|
|
277
|
+
]
|
|
278
|
+
else:
|
|
279
|
+
cmd = [
|
|
280
|
+
str(LOKI_CLI),
|
|
281
|
+
"start",
|
|
282
|
+
"--provider", req.provider,
|
|
283
|
+
prd_path,
|
|
284
|
+
]
|
|
280
285
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
try:
|
|
287
|
+
proc = subprocess.Popen(
|
|
288
|
+
cmd,
|
|
289
|
+
stdout=subprocess.PIPE,
|
|
290
|
+
stderr=subprocess.STDOUT,
|
|
291
|
+
stdin=subprocess.DEVNULL,
|
|
292
|
+
text=True,
|
|
293
|
+
cwd=project_dir,
|
|
294
|
+
env={**os.environ, "LOKI_DIR": os.path.join(project_dir, ".loki")},
|
|
295
|
+
start_new_session=True, # create new process group for clean kill
|
|
296
|
+
)
|
|
297
|
+
except FileNotFoundError:
|
|
298
|
+
return JSONResponse(
|
|
299
|
+
status_code=500,
|
|
300
|
+
content={"error": f"loki CLI not found at {LOKI_CLI}"},
|
|
301
|
+
)
|
|
302
|
+
except Exception as e:
|
|
303
|
+
return JSONResponse(
|
|
304
|
+
status_code=500,
|
|
305
|
+
content={"error": f"Failed to start session: {e}"},
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Update session state
|
|
309
|
+
session.reset()
|
|
310
|
+
session.process = proc
|
|
311
|
+
session.running = True
|
|
312
|
+
session.provider = req.provider
|
|
313
|
+
session.prd_text = req.prd
|
|
314
|
+
session.project_dir = project_dir
|
|
315
|
+
session.start_time = time.time()
|
|
289
316
|
|
|
290
|
-
|
|
291
|
-
|
|
317
|
+
# Start background output reader
|
|
318
|
+
session._reader_task = asyncio.create_task(_read_process_output())
|
|
292
319
|
|
|
293
320
|
await _broadcast({"type": "session_start", "data": {
|
|
294
321
|
"provider": req.provider,
|
|
@@ -307,24 +334,48 @@ async def start_session(req: StartRequest) -> JSONResponse:
|
|
|
307
334
|
@app.post("/api/session/stop")
|
|
308
335
|
async def stop_session() -> JSONResponse:
|
|
309
336
|
"""Stop the current loki session."""
|
|
310
|
-
|
|
311
|
-
|
|
337
|
+
async with session._lock:
|
|
338
|
+
if not session.running or session.process is None:
|
|
339
|
+
return JSONResponse(content={"stopped": False, "message": "No session running"})
|
|
340
|
+
|
|
341
|
+
proc = session.process
|
|
342
|
+
|
|
343
|
+
# 1. Mark stopped first so reader task loop exits
|
|
344
|
+
session.running = False
|
|
345
|
+
|
|
346
|
+
# 2. Cancel reader task before killing process
|
|
347
|
+
await session.cleanup()
|
|
348
|
+
|
|
349
|
+
# 3. Kill the process group (catches child processes too)
|
|
350
|
+
try:
|
|
351
|
+
pgid = os.getpgid(proc.pid)
|
|
352
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
353
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
354
|
+
# Fallback: kill the process directly
|
|
355
|
+
try:
|
|
356
|
+
proc.terminate()
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
312
359
|
|
|
313
|
-
try:
|
|
314
|
-
# Send SIGTERM, then SIGKILL after 5s
|
|
315
|
-
session.process.terminate()
|
|
316
360
|
try:
|
|
317
|
-
|
|
361
|
+
proc.wait(timeout=5)
|
|
318
362
|
except subprocess.TimeoutExpired:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
363
|
+
try:
|
|
364
|
+
pgid = os.getpgid(proc.pid)
|
|
365
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
366
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
367
|
+
try:
|
|
368
|
+
proc.kill()
|
|
369
|
+
except Exception:
|
|
370
|
+
pass
|
|
371
|
+
try:
|
|
372
|
+
proc.wait(timeout=3)
|
|
373
|
+
except Exception:
|
|
374
|
+
pass
|
|
323
375
|
|
|
324
|
-
|
|
325
|
-
await _broadcast({"type": "session_end", "data": {"message": "Session stopped by user"}})
|
|
376
|
+
await _broadcast({"type": "session_end", "data": {"message": "Session stopped by user"}})
|
|
326
377
|
|
|
327
|
-
|
|
378
|
+
return JSONResponse(content={"stopped": True, "message": "Session stopped"})
|
|
328
379
|
|
|
329
380
|
|
|
330
381
|
@app.get("/api/session/status")
|
|
@@ -607,9 +658,6 @@ def _run_loki_cmd(args: list, cwd: Optional[str] = None, timeout: int = 60) -> t
|
|
|
607
658
|
return (1, str(e))
|
|
608
659
|
|
|
609
660
|
|
|
610
|
-
_MAX_PRD_BYTES = 1_048_576 # 1 MB
|
|
611
|
-
|
|
612
|
-
|
|
613
661
|
@app.post("/api/session/plan")
|
|
614
662
|
async def plan_session(req: PlanRequest) -> JSONResponse:
|
|
615
663
|
"""Run loki plan dry-run analysis and return structured result."""
|
|
@@ -620,7 +668,7 @@ async def plan_session(req: PlanRequest) -> JSONResponse:
|
|
|
620
668
|
f.write(req.prd)
|
|
621
669
|
prd_tmp = f.name
|
|
622
670
|
try:
|
|
623
|
-
rc, output = await asyncio.
|
|
671
|
+
rc, output = await asyncio.get_running_loop().run_in_executor(
|
|
624
672
|
None, lambda: _run_loki_cmd(["plan", prd_tmp], timeout=90)
|
|
625
673
|
)
|
|
626
674
|
finally:
|
|
@@ -671,7 +719,7 @@ async def plan_session(req: PlanRequest) -> JSONResponse:
|
|
|
671
719
|
async def generate_report(req: ReportRequest) -> JSONResponse:
|
|
672
720
|
"""Run loki report and return content."""
|
|
673
721
|
fmt = req.format if req.format in ("html", "markdown") else "markdown"
|
|
674
|
-
rc, output = await asyncio.
|
|
722
|
+
rc, output = await asyncio.get_running_loop().run_in_executor(
|
|
675
723
|
None, lambda: _run_loki_cmd(["report", "--format", fmt], timeout=60)
|
|
676
724
|
)
|
|
677
725
|
return JSONResponse(content={
|
|
@@ -684,7 +732,7 @@ async def generate_report(req: ReportRequest) -> JSONResponse:
|
|
|
684
732
|
@app.post("/api/session/share")
|
|
685
733
|
async def share_session() -> JSONResponse:
|
|
686
734
|
"""Run loki share and return Gist URL."""
|
|
687
|
-
rc, output = await asyncio.
|
|
735
|
+
rc, output = await asyncio.get_running_loop().run_in_executor(
|
|
688
736
|
None, lambda: _run_loki_cmd(["share"], timeout=60)
|
|
689
737
|
)
|
|
690
738
|
# Try to extract URL from output
|
|
@@ -748,7 +796,7 @@ async def set_provider(req: ProviderSetRequest) -> JSONResponse:
|
|
|
748
796
|
@app.get("/api/session/metrics")
|
|
749
797
|
async def get_metrics() -> JSONResponse:
|
|
750
798
|
"""Run loki metrics --json and return parsed output."""
|
|
751
|
-
rc, output = await asyncio.
|
|
799
|
+
rc, output = await asyncio.get_running_loop().run_in_executor(
|
|
752
800
|
None, lambda: _run_loki_cmd(["metrics", "--json"], timeout=30)
|
|
753
801
|
)
|
|
754
802
|
# Try JSON parse
|
|
@@ -782,14 +830,67 @@ async def get_metrics() -> JSONResponse:
|
|
|
782
830
|
return JSONResponse(content=metrics)
|
|
783
831
|
|
|
784
832
|
|
|
833
|
+
def _infer_session_status(entry: Path) -> str:
|
|
834
|
+
"""Infer session status from project directory contents."""
|
|
835
|
+
# 1. Check .loki/state/session.json for explicit phase
|
|
836
|
+
state_file = entry / ".loki" / "state" / "session.json"
|
|
837
|
+
if state_file.exists():
|
|
838
|
+
try:
|
|
839
|
+
with open(state_file) as f:
|
|
840
|
+
st = json.load(f)
|
|
841
|
+
phase = st.get("phase", "")
|
|
842
|
+
if phase and phase != "idle":
|
|
843
|
+
return phase
|
|
844
|
+
except (json.JSONDecodeError, OSError):
|
|
845
|
+
pass
|
|
846
|
+
|
|
847
|
+
# 2. Check .loki/autonomy-state.json (run.sh writes this)
|
|
848
|
+
for state_name in ("autonomy-state.json", ".loki/autonomy-state.json"):
|
|
849
|
+
sf = entry / state_name
|
|
850
|
+
if sf.exists():
|
|
851
|
+
try:
|
|
852
|
+
with open(sf) as f:
|
|
853
|
+
st = json.load(f)
|
|
854
|
+
if st.get("completed") or st.get("status") == "completed":
|
|
855
|
+
return "completed"
|
|
856
|
+
if st.get("status"):
|
|
857
|
+
return st["status"]
|
|
858
|
+
except (json.JSONDecodeError, OSError):
|
|
859
|
+
pass
|
|
860
|
+
|
|
861
|
+
# 3. Infer from file contents
|
|
862
|
+
files = set()
|
|
863
|
+
try:
|
|
864
|
+
files = {f.name for f in entry.iterdir() if not f.name.startswith(".")}
|
|
865
|
+
except OSError:
|
|
866
|
+
pass
|
|
867
|
+
|
|
868
|
+
source_extensions = {".js", ".ts", ".tsx", ".py", ".html", ".css", ".go", ".rs", ".java", ".rb"}
|
|
869
|
+
has_source = any(
|
|
870
|
+
(entry / f).suffix in source_extensions
|
|
871
|
+
for f in files
|
|
872
|
+
if (entry / f).is_file()
|
|
873
|
+
)
|
|
874
|
+
has_prd = "PRD.md" in files or "prd.md" in files
|
|
875
|
+
|
|
876
|
+
if has_source:
|
|
877
|
+
return "completed"
|
|
878
|
+
if has_prd and len(files) <= 2:
|
|
879
|
+
return "started"
|
|
880
|
+
if has_prd:
|
|
881
|
+
return "in_progress"
|
|
882
|
+
|
|
883
|
+
return "empty"
|
|
884
|
+
|
|
885
|
+
|
|
785
886
|
@app.get("/api/sessions/history")
|
|
786
887
|
async def get_sessions_history() -> JSONResponse:
|
|
787
|
-
"""Return list of past loki sessions from
|
|
888
|
+
"""Return list of past loki sessions from ~/purple-lab-projects/ etc."""
|
|
788
889
|
history: list[dict] = []
|
|
789
890
|
search_dirs = [
|
|
891
|
+
Path.home() / "purple-lab-projects",
|
|
790
892
|
Path.home() / ".loki-sessions",
|
|
791
893
|
Path.home() / ".loki" / "sessions",
|
|
792
|
-
Path.home() / "purple-lab-projects",
|
|
793
894
|
]
|
|
794
895
|
for base_dir in search_dirs:
|
|
795
896
|
if not base_dir.is_dir():
|
|
@@ -802,7 +903,7 @@ async def get_sessions_history() -> JSONResponse:
|
|
|
802
903
|
"path": str(entry),
|
|
803
904
|
"date": "",
|
|
804
905
|
"prd_snippet": "",
|
|
805
|
-
"status":
|
|
906
|
+
"status": _infer_session_status(entry),
|
|
806
907
|
}
|
|
807
908
|
# Read timestamp from directory mtime
|
|
808
909
|
try:
|
|
@@ -821,21 +922,83 @@ async def get_sessions_history() -> JSONResponse:
|
|
|
821
922
|
except OSError:
|
|
822
923
|
pass
|
|
823
924
|
break
|
|
824
|
-
#
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
925
|
+
# Count project files for progress indication
|
|
926
|
+
try:
|
|
927
|
+
file_count = sum(1 for f in entry.rglob("*") if f.is_file()
|
|
928
|
+
and ".git" not in f.parts and "node_modules" not in f.parts
|
|
929
|
+
and "__pycache__" not in f.parts)
|
|
930
|
+
session_info["file_count"] = file_count
|
|
931
|
+
except OSError:
|
|
932
|
+
session_info["file_count"] = 0
|
|
933
|
+
|
|
833
934
|
history.append(session_info)
|
|
834
935
|
if history:
|
|
835
936
|
break # Use first directory that has entries
|
|
836
937
|
return JSONResponse(content=history)
|
|
837
938
|
|
|
838
939
|
|
|
940
|
+
@app.get("/api/sessions/{session_id}")
|
|
941
|
+
async def get_session_detail(session_id: str) -> JSONResponse:
|
|
942
|
+
"""Get details of a past session for read-only viewing."""
|
|
943
|
+
import re
|
|
944
|
+
# Validate session_id format (prevent path traversal)
|
|
945
|
+
if not re.match(r"^[a-zA-Z0-9_-]+$", session_id):
|
|
946
|
+
return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
|
|
947
|
+
|
|
948
|
+
search_dirs = [
|
|
949
|
+
Path.home() / "purple-lab-projects",
|
|
950
|
+
Path.home() / ".loki-sessions",
|
|
951
|
+
Path.home() / ".loki" / "sessions",
|
|
952
|
+
]
|
|
953
|
+
target: Optional[Path] = None
|
|
954
|
+
for base_dir in search_dirs:
|
|
955
|
+
candidate = base_dir / session_id
|
|
956
|
+
if candidate.is_dir():
|
|
957
|
+
target = candidate
|
|
958
|
+
break
|
|
959
|
+
|
|
960
|
+
if target is None:
|
|
961
|
+
return JSONResponse(status_code=404, content={"error": "Session not found"})
|
|
962
|
+
|
|
963
|
+
# Read PRD
|
|
964
|
+
prd_content = ""
|
|
965
|
+
for prd_name in ("PRD.md", "prd.md", ".loki/prd.md"):
|
|
966
|
+
prd_file = target / prd_name
|
|
967
|
+
if prd_file.exists():
|
|
968
|
+
try:
|
|
969
|
+
prd_content = prd_file.read_text(errors="replace")
|
|
970
|
+
except OSError:
|
|
971
|
+
pass
|
|
972
|
+
break
|
|
973
|
+
|
|
974
|
+
# Build file tree
|
|
975
|
+
files = _build_file_tree(target)
|
|
976
|
+
|
|
977
|
+
# Read logs if available
|
|
978
|
+
log_lines: list[str] = []
|
|
979
|
+
for log_name in (".loki/logs/session.log", ".loki/session.log", "loki.log"):
|
|
980
|
+
log_file = target / log_name
|
|
981
|
+
if log_file.exists():
|
|
982
|
+
try:
|
|
983
|
+
text = log_file.read_text(errors="replace")
|
|
984
|
+
log_lines = text.splitlines()[-200:]
|
|
985
|
+
except OSError:
|
|
986
|
+
pass
|
|
987
|
+
break
|
|
988
|
+
|
|
989
|
+
# Status
|
|
990
|
+
status = _infer_session_status(target)
|
|
991
|
+
|
|
992
|
+
return JSONResponse(content={
|
|
993
|
+
"id": session_id,
|
|
994
|
+
"path": str(target),
|
|
995
|
+
"status": status,
|
|
996
|
+
"prd": prd_content,
|
|
997
|
+
"files": files,
|
|
998
|
+
"logs": log_lines,
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
|
|
839
1002
|
@app.post("/api/session/onboard")
|
|
840
1003
|
async def onboard_session(req: OnboardRequest) -> JSONResponse:
|
|
841
1004
|
"""Run loki onboard on a path and return CLAUDE.md content."""
|
|
@@ -852,7 +1015,7 @@ async def onboard_session(req: OnboardRequest) -> JSONResponse:
|
|
|
852
1015
|
if not target.is_dir():
|
|
853
1016
|
return JSONResponse(status_code=400, content={"error": "Path must be a directory"})
|
|
854
1017
|
|
|
855
|
-
rc, output = await asyncio.
|
|
1018
|
+
rc, output = await asyncio.get_running_loop().run_in_executor(
|
|
856
1019
|
None, lambda: _run_loki_cmd(["onboard", str(target)], cwd=str(target), timeout=120)
|
|
857
1020
|
)
|
|
858
1021
|
# Try to read generated CLAUDE.md
|
|
@@ -870,6 +1033,17 @@ async def onboard_session(req: OnboardRequest) -> JSONResponse:
|
|
|
870
1033
|
})
|
|
871
1034
|
|
|
872
1035
|
|
|
1036
|
+
# ---------------------------------------------------------------------------
|
|
1037
|
+
# Health check
|
|
1038
|
+
# ---------------------------------------------------------------------------
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
@app.get("/health")
|
|
1042
|
+
async def health_check() -> JSONResponse:
|
|
1043
|
+
"""Health check for load balancers and orchestrators."""
|
|
1044
|
+
return JSONResponse(content={"status": "ok", "service": "purple-lab"})
|
|
1045
|
+
|
|
1046
|
+
|
|
873
1047
|
# ---------------------------------------------------------------------------
|
|
874
1048
|
# WebSocket
|
|
875
1049
|
# ---------------------------------------------------------------------------
|
|
@@ -1011,6 +1185,10 @@ async def websocket_endpoint(ws: WebSocket) -> None:
|
|
|
1011
1185
|
pass
|
|
1012
1186
|
finally:
|
|
1013
1187
|
push_task.cancel()
|
|
1188
|
+
try:
|
|
1189
|
+
await push_task
|
|
1190
|
+
except (asyncio.CancelledError, Exception):
|
|
1191
|
+
pass
|
|
1014
1192
|
session.ws_clients.discard(ws)
|
|
1015
1193
|
|
|
1016
1194
|
|
|
@@ -1 +0,0 @@
|
|
|
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:Inter,system-ui,-apple-system,sans-serif;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:JetBrains Mono,Fira Code,Cascadia Code,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}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.terminal-scroll::-webkit-scrollbar{width:6px}.terminal-scroll::-webkit-scrollbar-track{background:transparent}.terminal-scroll::-webkit-scrollbar-thumb{background:#3d52a033;border-radius:3px}.terminal-scroll::-webkit-scrollbar-thumb:hover{background:#3d52a059}.glass{background:#fff9;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,.3);border-radius:16px;box-shadow:0 8px 32px #3d52a014}.glass-subtle{background:#fff6;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,.2);border-radius:12px;box-shadow:0 4px 16px #3d52a00f}.pattern-circles{position:absolute;top:0;right:0;bottom:0;left:0;opacity:.04;background-image:radial-gradient(circle at 20% 30%,transparent 30%,#6C63FF 30.5%,transparent 31%),radial-gradient(circle at 80% 70%,transparent 25%,#6C63FF 25.5%,transparent 26%),radial-gradient(circle at 50% 50%,transparent 40%,#6C63FF 40.5%,transparent 41%);pointer-events:none}@keyframes phase-pulse{0%,to{opacity:1}50%{opacity:.5}}.phase-active{animation:phase-pulse 2s ease-in-out infinite}@keyframes cursor-blink{0%,to{opacity:1}50%{opacity:0}}.terminal-cursor:after{content:"";display:inline-block;width:8px;height:16px;background:#7091e6;animation:cursor-blink 1s step-end infinite;margin-left:2px;vertical-align:text-bottom}.\!visible{visibility:visible!important}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.right-0{right:0}.top-0{top:0}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.col-span-3{grid-column:span 3 / span 3}.col-span-4{grid-column:span 4 / span 4}.col-span-5{grid-column:span 5 / span 5}.col-span-6{grid-column:span 6 / span 6}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.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}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.h-1\.5{height:.375rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-28{height:7rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-full{height:100%}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-\[400px\]{max-height:400px}.min-h-0{min-height:0px}.min-h-\[280px\]{min-height:280px}.min-h-\[300px\]{min-height:300px}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-1\/2{width:50%}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-28{width:7rem}.w-3{width:.75rem}.w-4{width:1rem}.w-56{width:14rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.max-w-3xl{max-width:48rem}.max-w-\[1920px\]{max-width:1920px}.max-w-\[220px\]{max-width:220px}.max-w-lg{max-width:32rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,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}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0px}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.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))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-accent-product{--tw-border-opacity: 1;border-color:rgb(108 99 255 / var(--tw-border-opacity, 1))}.border-accent-product\/20{border-color:#6c63ff33}.border-accent-product\/30{border-color:#6c63ff4d}.border-danger\/20{border-color:#c45b5b33}.border-primary-wash\/30{border-color:#8697c44d}.border-primary\/20{border-color:#3d52a033}.border-slate\/20{border-color:#4f5d7533}.border-success\/20{border-color:#7fb06933}.border-surface\/50{border-color:#adbbda80}.border-warning\/20{border-color:#d4a03c33}.border-warning\/30{border-color:#d4a03c4d}.border-warning\/40{border-color:#d4a03c66}.border-white\/10{border-color:#ffffff1a}.border-white\/20{border-color:#fff3}.border-white\/30{border-color:#ffffff4d}.border-t-transparent{border-top-color:transparent}.bg-accent-product{--tw-bg-opacity: 1;background-color:rgb(108 99 255 / var(--tw-bg-opacity, 1))}.bg-accent-product\/10{background-color:#6c63ff1a}.bg-background{--tw-bg-opacity: 1;background-color:rgb(237 232 245 / var(--tw-bg-opacity, 1))}.bg-black\/30{background-color:#0000004d}.bg-black\/5{background-color:#0000000d}.bg-charcoal\/10{background-color:#2d31421a}.bg-charcoal\/5{background-color:#2d31420d}.bg-charcoal\/\[0\.03\]{background-color:#2d314208}.bg-danger{--tw-bg-opacity: 1;background-color:rgb(196 91 91 / var(--tw-bg-opacity, 1))}.bg-danger\/10{background-color:#c45b5b1a}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(61 82 160 / var(--tw-bg-opacity, 1))}.bg-primary-light{--tw-bg-opacity: 1;background-color:rgb(112 145 230 / var(--tw-bg-opacity, 1))}.bg-primary-wash\/20{background-color:#8697c433}.bg-primary\/10{background-color:#3d52a01a}.bg-slate{--tw-bg-opacity: 1;background-color:rgb(79 93 117 / var(--tw-bg-opacity, 1))}.bg-slate\/10{background-color:#4f5d751a}.bg-slate\/30{background-color:#4f5d754d}.bg-slate\/40{background-color:#4f5d7566}.bg-success{--tw-bg-opacity: 1;background-color:rgb(127 176 105 / var(--tw-bg-opacity, 1))}.bg-success\/10{background-color:#7fb0691a}.bg-surface{--tw-bg-opacity: 1;background-color:rgb(173 187 218 / var(--tw-bg-opacity, 1))}.bg-surface\/30{background-color:#adbbda4d}.bg-surface\/50{background-color:#adbbda80}.bg-warning{--tw-bg-opacity: 1;background-color:rgb(212 160 60 / var(--tw-bg-opacity, 1))}.bg-warning\/10{background-color:#d4a03c1a}.bg-warning\/5{background-color:#d4a03c0d}.bg-white\/30{background-color:#ffffff4d}.bg-white\/40{background-color:#fff6}.fill-charcoal{fill:#2d3142}.fill-primary{fill:#3d52a0}.p-0{padding:0}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2\.5{padding-bottom:.625rem}.pr-2{padding-right:.5rem}.pt-0{padding-top:0}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,Fira Code,Cascadia Code,monospace}.font-sans{font-family:Inter,system-ui,-apple-system,sans-serif}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.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}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-relaxed{line-height:1.625}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-accent-product{--tw-text-opacity: 1;color:rgb(108 99 255 / var(--tw-text-opacity, 1))}.text-charcoal{--tw-text-opacity: 1;color:rgb(45 49 66 / var(--tw-text-opacity, 1))}.text-charcoal\/80{color:#2d3142cc}.text-danger{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.text-primary{--tw-text-opacity: 1;color:rgb(61 82 160 / var(--tw-text-opacity, 1))}.text-primary-light{--tw-text-opacity: 1;color:rgb(112 145 230 / var(--tw-text-opacity, 1))}.text-primary-wash{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.text-slate{--tw-text-opacity: 1;color:rgb(79 93 117 / var(--tw-text-opacity, 1))}.text-slate\/40{color:#4f5d7566}.text-slate\/50{color:#4f5d7580}.text-slate\/60{color:#4f5d7599}.text-success{--tw-text-opacity: 1;color:rgb(127 176 105 / var(--tw-text-opacity, 1))}.text-warning{--tw-text-opacity: 1;color:rgb(212 160 60 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-glass{--tw-shadow: 0 8px 32px rgba(61,82,160,.08);--tw-shadow-colored: 0 8px 32px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-glass-subtle{--tw-shadow: 0 4px 16px rgba(61,82,160,.06);--tw-shadow-colored: 0 4px 16px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 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)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.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}.duration-200{transition-duration:.2s}.duration-500{transition-duration:.5s}.placeholder\:text-primary-wash::-moz-placeholder{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.placeholder\:text-primary-wash::placeholder{--tw-text-opacity: 1;color:rgb(134 151 196 / var(--tw-text-opacity, 1))}.placeholder\:text-primary-wash\/70::-moz-placeholder{color:#8697c4b3}.placeholder\:text-primary-wash\/70::placeholder{color:#8697c4b3}.hover\:bg-accent-product\/20:hover{background-color:#6c63ff33}.hover\:bg-accent-product\/5:hover{background-color:#6c63ff0d}.hover\:bg-accent-product\/90:hover{background-color:#6c63ffe6}.hover\:bg-danger\/20:hover{background-color:#c45b5b33}.hover\:bg-primary\/5:hover{background-color:#3d52a00d}.hover\:bg-warning\/10:hover{background-color:#d4a03c1a}.hover\:bg-white\/20:hover{background-color:#fff3}.hover\:bg-white\/30:hover{background-color:#ffffff4d}.hover\:bg-white\/40:hover{background-color:#fff6}.hover\:bg-white\/5:hover{background-color:#ffffff0d}.hover\:text-charcoal:hover{--tw-text-opacity: 1;color:rgb(45 49 66 / var(--tw-text-opacity, 1))}.hover\:text-primary-light:hover{--tw-text-opacity: 1;color:rgb(112 145 230 / var(--tw-text-opacity, 1))}.focus\:border-accent-product\/30:focus{border-color:#6c63ff4d}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-accent-product\/20:focus{--tw-ring-color: rgb(108 99 255 / .2)}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-accent-product{--tw-text-opacity: 1;color:rgb(108 99 255 / var(--tw-text-opacity, 1))}
|