loki-mode 6.44.1 → 6.45.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +30 -18
- 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/{Badge-DTFBYZoA.js → Badge-ClncXa1x.js} +1 -1
- package/web-app/dist/assets/Button-CUOgrX10.js +6 -0
- package/web-app/dist/assets/{Card-CL6xo_RX.js → Card-2UWDXe0P.js} +1 -1
- package/web-app/dist/assets/{HomePage-1Ccs1i0m.js → HomePage-C4WxoEKI.js} +1 -1
- package/web-app/dist/assets/ProjectPage-Dy7ONtf_.js +162 -0
- package/web-app/dist/assets/ProjectsPage-CIDkRHyZ.js +6 -0
- package/web-app/dist/assets/{SettingsPage-CULNMl6W.js → SettingsPage-5AxjoTTg.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DdrBEy9w.js → TemplatesPage-DPlaAtAk.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-C3fQIXqq.js → TerminalOutput-Do2PhilR.js} +1 -1
- package/web-app/dist/assets/{clock-D5NTAxeP.js → clock-DpWpY1Zx.js} +1 -1
- package/web-app/dist/assets/{external-link-CSO4H-LC.js → external-link-KmF9dPsz.js} +1 -1
- package/web-app/dist/assets/{index-DvhjaUe4.js → index-ACgjqVp2.js} +17 -17
- package/web-app/dist/assets/index-BZpAV5M8.css +1 -0
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +353 -63
- package/web-app/dist/assets/Button-NSmP6YCA.js +0 -1
- package/web-app/dist/assets/ProjectPage-wby1itP1.js +0 -141
- package/web-app/dist/assets/ProjectsPage-DEP-ZiW7.js +0 -11
- package/web-app/dist/assets/index-BLBd5LBk.css +0 -1
|
@@ -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:#553de933;border-radius:3px}.terminal-scroll::-webkit-scrollbar-thumb:hover{background:#553de959}.card{background:#fff;border:1px solid #ECEAE3;border-radius:5px;box-shadow:0 1px 3px #0000000f}.card:hover,.card-hover:hover{box-shadow:0 5px 10px #00000014}.pattern-nodes{position:absolute;top:0;right:0;bottom:0;left:0;opacity:.04;pointer-events:none;background-image:radial-gradient(circle at 10% 20%,#553DE9 1px,transparent 1px),radial-gradient(circle at 30% 60%,#553DE9 1px,transparent 1px),radial-gradient(circle at 50% 40%,#553DE9 1px,transparent 1px),radial-gradient(circle at 70% 80%,#553DE9 1px,transparent 1px),radial-gradient(circle at 90% 30%,#553DE9 1px,transparent 1px);background-size:200px 200px}*:focus-visible{outline:2px solid #553DE9;outline-offset:2px}::-moz-selection{background:#e8e4fd;color:#201515}::selection{background:#e8e4fd;color:#201515}@media(prefers-reduced-motion:reduce){.phase-active{animation: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:#553de9;animation:cursor-blink 1s step-end infinite;margin-left:2px;vertical-align:text-bottom}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.\!visible{visibility:visible!important}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.bottom-0{bottom:0}.left-3{left:.75rem}.right-0{right:0}.top-1\/2{top:50%}.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}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-10{margin-bottom:2.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.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}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-28{height:7rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.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-screen{min-height:100vh}.w-1{width:.25rem}.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-20{width:5rem}.w-28{width:7rem}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[160px\]{min-width:160px}.max-w-3xl{max-width:48rem}.max-w-\[1400px\]{max-width:1400px}.max-w-\[1920px\]{max-width:1920px}.max-w-\[200px\]{max-width:200px}.max-w-\[220px\]{max-width:220px}.max-w-\[80\%\]{max-width:80%}.max-w-\[800px\]{max-width:800px}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -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))}@keyframes ping{75%,to{transform:scale(2);opacity:0}}.animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}@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-col-resize{cursor:col-resize}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.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-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.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-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.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))}.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))}.overflow-auto{overflow:auto}.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}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-\[3px\]{border-radius:3px}.rounded-\[5px\]{border-radius:5px}.rounded-btn{border-radius:4px}.rounded-card{border-radius:5px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-t-card{border-top-left-radius:5px;border-top-right-radius:5px}.border{border-width:1px}.border-0{border-width:0px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.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-\[\#553DE9\]{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.border-\[\#553DE9\]\/30{border-color:#553de94d}.border-\[\#C45B5B\]\/20{border-color:#c45b5b33}.border-\[\#ECEAE3\],.border-border{--tw-border-opacity: 1;border-color:rgb(236 234 227 / var(--tw-border-opacity, 1))}.border-border-light{--tw-border-opacity: 1;border-color:rgb(197 192 177 / var(--tw-border-opacity, 1))}.border-border\/50{border-color:#eceae380}.border-danger\/20{border-color:#c45b5b33}.border-muted\/20{border-color:#93908433}.border-primary{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.border-primary\/20{border-color:#553de933}.border-primary\/30{border-color:#553de94d}.border-primary\/40{border-color:#553de966}.border-success\/20{border-color:#1fc5a833}.border-transparent{border-color:transparent}.border-warning\/10{border-color:#d4a03c1a}.border-warning\/20{border-color:#d4a03c33}.border-warning\/30{border-color:#d4a03c4d}.border-warning\/40{border-color:#d4a03c66}.border-t-transparent{border-top-color:transparent}.bg-\[\#1FC5A8\]{--tw-bg-opacity: 1;background-color:rgb(31 197 168 / var(--tw-bg-opacity, 1))}.bg-\[\#1FC5A8\]\/10{background-color:#1fc5a81a}.bg-\[\#553DE9\]{--tw-bg-opacity: 1;background-color:rgb(85 61 233 / var(--tw-bg-opacity, 1))}.bg-\[\#553DE9\]\/10{background-color:#553de91a}.bg-\[\#C45B5B\]{--tw-bg-opacity: 1;background-color:rgb(196 91 91 / var(--tw-bg-opacity, 1))}.bg-\[\#C45B5B\]\/10{background-color:#c45b5b1a}.bg-\[\#D4A03C\]\/10{background-color:#d4a03c1a}.bg-\[\#F8F4F0\]{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.bg-\[\#FAF9F6\]{--tw-bg-opacity: 1;background-color:rgb(250 249 246 / var(--tw-bg-opacity, 1))}.bg-background{--tw-bg-opacity: 1;background-color:rgb(255 254 251 / var(--tw-bg-opacity, 1))}.bg-black\/30{background-color:#0000004d}.bg-black\/5{background-color:#0000000d}.bg-border{--tw-bg-opacity: 1;background-color:rgb(236 234 227 / var(--tw-bg-opacity, 1))}.bg-border\/30{background-color:#eceae34d}.bg-card{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-current{background-color:currentColor}.bg-danger{--tw-bg-opacity: 1;background-color:rgb(196 91 91 / var(--tw-bg-opacity, 1))}.bg-danger\/10{background-color:#c45b5b1a}.bg-hover{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.bg-info{--tw-bg-opacity: 1;background-color:rgb(47 113 227 / var(--tw-bg-opacity, 1))}.bg-ink\/\[0\.03\]{background-color:#20151508}.bg-muted{--tw-bg-opacity: 1;background-color:rgb(147 144 132 / var(--tw-bg-opacity, 1))}.bg-muted\/10{background-color:#9390841a}.bg-muted\/30{background-color:#9390844d}.bg-muted\/40{background-color:#93908466}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(85 61 233 / var(--tw-bg-opacity, 1))}.bg-primary\/10{background-color:#553de91a}.bg-success{--tw-bg-opacity: 1;background-color:rgb(31 197 168 / var(--tw-bg-opacity, 1))}.bg-success\/10{background-color:#1fc5a81a}.bg-transparent{background-color:transparent}.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{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.fill-ink{fill:#201515}.fill-primary{fill:#553de9}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.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-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-20{padding-top:5rem;padding-bottom:5rem}.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}.pl-3{padding-left:.75rem}.pl-9{padding-left:2.25rem}.pr-2{padding-right:.5rem}.pr-3{padding-right:.75rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-\[20vh\]{padding-top:20vh}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-heading{font-family:DM Serif Display,Georgia,Times New Roman,serif}.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-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-h1{font-size:2.5rem;line-height:1;letter-spacing:-.01em}.text-h3{font-size:1.25rem;line-height:1.4}.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}.leading-tight{line-height:1.25}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#1FC5A8\]{--tw-text-opacity: 1;color:rgb(31 197 168 / var(--tw-text-opacity, 1))}.text-\[\#36342E\]{--tw-text-opacity: 1;color:rgb(54 52 46 / var(--tw-text-opacity, 1))}.text-\[\#553DE9\]{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.text-\[\#6B6960\]{--tw-text-opacity: 1;color:rgb(107 105 96 / var(--tw-text-opacity, 1))}.text-\[\#939084\]{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.text-\[\#C45B5B\]{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.text-\[\#D4A03C\]{--tw-text-opacity: 1;color:rgb(212 160 60 / var(--tw-text-opacity, 1))}.text-cyan-600{--tw-text-opacity: 1;color:rgb(8 145 178 / var(--tw-text-opacity, 1))}.text-danger{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-info{--tw-text-opacity: 1;color:rgb(47 113 227 / var(--tw-text-opacity, 1))}.text-ink{--tw-text-opacity: 1;color:rgb(32 21 21 / var(--tw-text-opacity, 1))}.text-ink\/70{color:#201515b3}.text-muted{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.text-muted-accessible{--tw-text-opacity: 1;color:rgb(107 105 96 / var(--tw-text-opacity, 1))}.text-muted\/50{color:#93908480}.text-muted\/60{color:#93908499}.text-orange-500{--tw-text-opacity: 1;color:rgb(249 115 22 / var(--tw-text-opacity, 1))}.text-orange-600{--tw-text-opacity: 1;color:rgb(234 88 12 / var(--tw-text-opacity, 1))}.text-primary{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.text-primary\/60{color:#553de999}.text-purple-500{--tw-text-opacity: 1;color:rgb(168 85 247 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-secondary{--tw-text-opacity: 1;color:rgb(54 52 46 / var(--tw-text-opacity, 1))}.text-success{--tw-text-opacity: 1;color:rgb(31 197 168 / 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))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px 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-button{--tw-shadow: 0 1px 3px rgba(0,0,0,.08);--tw-shadow-colored: 0 1px 3px 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-card{--tw-shadow: 0 1px 3px rgba(0,0,0,.06);--tw-shadow-colored: 0 1px 3px 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-card-hover{--tw-shadow: 0 5px 10px rgba(0,0,0,.08);--tw-shadow-colored: 0 5px 10px 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-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)}.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)}.shadow-\[\#553DE9\]\/20{--tw-shadow-color: rgb(85 61 233 / .2);--tw-shadow: var(--tw-shadow-colored)}.shadow-card{--tw-shadow-color: #FFFFFF;--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-2{--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)}.ring-\[\#553DE9\]{--tw-ring-opacity: 1;--tw-ring-color: rgb(85 61 233 / var(--tw-ring-opacity, 1))}.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-\[width\]{transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.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-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-500{transition-duration:.5s}.placeholder\:text-\[\#939084\]::-moz-placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-\[\#939084\]::placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-muted::-moz-placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-muted::placeholder{--tw-text-opacity: 1;color:rgb(147 144 132 / var(--tw-text-opacity, 1))}.placeholder\:text-primary\/60::-moz-placeholder{color:#553de999}.placeholder\:text-primary\/60::placeholder{color:#553de999}.last\:border-b-0:last-child{border-bottom-width:0px}.hover\:border-border:hover{--tw-border-opacity: 1;border-color:rgb(236 234 227 / var(--tw-border-opacity, 1))}.hover\:border-primary\/30:hover{border-color:#553de94d}.hover\:bg-\[\#4432c4\]:hover{--tw-bg-opacity: 1;background-color:rgb(68 50 196 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#553DE9\]\/5:hover{background-color:#553de90d}.hover\:bg-\[\#553DE9\]\/90:hover{background-color:#553de9e6}.hover\:bg-\[\#C45B5B\]\/20:hover{background-color:#c45b5b33}.hover\:bg-\[\#E8E4FD\]:hover{--tw-bg-opacity: 1;background-color:rgb(232 228 253 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#ECEAE3\]:hover{--tw-bg-opacity: 1;background-color:rgb(236 234 227 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#F8F4F0\]:hover{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.hover\:bg-card:hover{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.hover\:bg-danger\/10:hover{background-color:#c45b5b1a}.hover\:bg-danger\/20:hover{background-color:#c45b5b33}.hover\:bg-hover:hover{--tw-bg-opacity: 1;background-color:rgb(248 244 240 / var(--tw-bg-opacity, 1))}.hover\:bg-primary\/20:hover{background-color:#553de933}.hover\:bg-primary\/30:hover{background-color:#553de94d}.hover\:bg-primary\/5:hover{background-color:#553de90d}.hover\:bg-primary\/90:hover{background-color:#553de9e6}.hover\:bg-warning\/10:hover{background-color:#d4a03c1a}.hover\:text-\[\#36342E\]:hover{--tw-text-opacity: 1;color:rgb(54 52 46 / var(--tw-text-opacity, 1))}.hover\:text-danger:hover{--tw-text-opacity: 1;color:rgb(196 91 91 / var(--tw-text-opacity, 1))}.hover\:text-ink:hover{--tw-text-opacity: 1;color:rgb(32 21 21 / var(--tw-text-opacity, 1))}.hover\:text-primary:hover{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.hover\:text-primary\/80:hover{color:#553de9cc}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-card-hover:hover{--tw-shadow: 0 5px 10px rgba(0,0,0,.08);--tw-shadow-colored: 0 5px 10px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:not-sr-only:focus{position:static;width:auto;height:auto;padding:0;margin:0;overflow:visible;clip:auto;white-space:normal}.focus\:absolute:focus{position:absolute}.focus\:left-2:focus{left:.5rem}.focus\:top-2:focus{top:.5rem}.focus\:z-50:focus{z-index:50}.focus\:rounded-\[3px\]:focus{border-radius:3px}.focus\:border-\[\#553DE9\]:focus{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.focus\:border-primary:focus{--tw-border-opacity: 1;border-color:rgb(85 61 233 / var(--tw-border-opacity, 1))}.focus\:border-primary\/30:focus{border-color:#553de94d}.focus\:bg-white:focus{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.focus\:px-4:focus{padding-left:1rem;padding-right:1rem}.focus\:py-2:focus{padding-top:.5rem;padding-bottom:.5rem}.focus\:text-\[\#553DE9\]:focus{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.focus\:shadow-card:focus{--tw-shadow: 0 1px 3px rgba(0,0,0,.06);--tw-shadow-colored: 0 1px 3px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);--tw-shadow-color: #FFFFFF;--tw-shadow: var(--tw-shadow-colored)}.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-\[\#553DE9\]\/20:focus{--tw-ring-color: rgb(85 61 233 / .2)}.focus\:ring-primary\/20:focus{--tw-ring-color: rgb(85 61 233 / .2)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-primary{--tw-text-opacity: 1;color:rgb(85 61 233 / var(--tw-text-opacity, 1))}.group\/file:hover .group-hover\/file\:opacity-100{opacity:1}@media(prefers-reduced-motion:reduce){.motion-reduce\:animate-none{animation:none}}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
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=DM+Serif+Display&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-ACgjqVp2.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BZpAV5M8.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body class="bg-background text-ink font-sans antialiased">
|
|
15
15
|
<div id="root"></div>
|
package/web-app/server.py
CHANGED
|
@@ -12,10 +12,12 @@ import asyncio
|
|
|
12
12
|
import inspect
|
|
13
13
|
import json
|
|
14
14
|
import os
|
|
15
|
+
import re
|
|
15
16
|
import signal
|
|
16
17
|
import subprocess
|
|
17
18
|
import sys
|
|
18
19
|
import time
|
|
20
|
+
import uuid
|
|
19
21
|
from pathlib import Path
|
|
20
22
|
from typing import Optional
|
|
21
23
|
|
|
@@ -109,46 +111,85 @@ class SessionState:
|
|
|
109
111
|
self.log_lines = []
|
|
110
112
|
|
|
111
113
|
|
|
112
|
-
def
|
|
113
|
-
"""Kill
|
|
114
|
+
def _kill_tracked_child_processes() -> None:
|
|
115
|
+
"""Kill only processes that Purple Lab started, not external loki sessions."""
|
|
114
116
|
import subprocess as _sp
|
|
115
|
-
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
patterns.append("loki-run-")
|
|
117
|
+
tracked = _get_tracked_child_pids()
|
|
118
|
+
if not tracked:
|
|
119
|
+
return
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
for pattern in patterns:
|
|
121
|
+
for pid in tracked:
|
|
122
122
|
try:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
for pid_str in result.stdout.strip().splitlines():
|
|
129
|
-
try:
|
|
130
|
-
pid = int(pid_str.strip())
|
|
131
|
-
if pid not in killed_pids:
|
|
132
|
-
os.kill(pid, signal.SIGTERM)
|
|
133
|
-
killed_pids.add(pid)
|
|
134
|
-
except (ValueError, ProcessLookupError, PermissionError):
|
|
135
|
-
pass
|
|
136
|
-
except Exception:
|
|
123
|
+
# Kill the entire process tree (children first, then parent)
|
|
124
|
+
_sp.run(["pkill", "-TERM", "-P", str(pid)],
|
|
125
|
+
capture_output=True, timeout=5)
|
|
126
|
+
os.kill(pid, signal.SIGTERM)
|
|
127
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
137
128
|
pass
|
|
138
129
|
|
|
139
|
-
# Wait briefly then SIGKILL
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
130
|
+
# Wait briefly then SIGKILL survivors
|
|
131
|
+
import time as _time
|
|
132
|
+
_time.sleep(2)
|
|
133
|
+
for pid in tracked:
|
|
134
|
+
try:
|
|
135
|
+
_sp.run(["pkill", "-9", "-P", str(pid)],
|
|
136
|
+
capture_output=True, timeout=5)
|
|
137
|
+
os.kill(pid, signal.SIGKILL)
|
|
138
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
_clear_tracked_pids()
|
|
148
142
|
|
|
149
143
|
|
|
150
144
|
session = SessionState()
|
|
151
145
|
|
|
146
|
+
# Track PIDs of sessions started by Purple Lab (not by external loki CLI)
|
|
147
|
+
_PURPLE_LAB_PIDS_FILE = SCRIPT_DIR.parent / ".loki" / "purple-lab" / "child-pids.json"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _track_child_pid(pid: int) -> None:
|
|
151
|
+
"""Record a PID started by Purple Lab so loki web stop can clean it up."""
|
|
152
|
+
_PURPLE_LAB_PIDS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
pids: list[int] = []
|
|
154
|
+
if _PURPLE_LAB_PIDS_FILE.exists():
|
|
155
|
+
try:
|
|
156
|
+
pids = json.loads(_PURPLE_LAB_PIDS_FILE.read_text())
|
|
157
|
+
except (json.JSONDecodeError, OSError):
|
|
158
|
+
pids = []
|
|
159
|
+
if pid not in pids:
|
|
160
|
+
pids.append(pid)
|
|
161
|
+
_PURPLE_LAB_PIDS_FILE.write_text(json.dumps(pids))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _untrack_child_pid(pid: int) -> None:
|
|
165
|
+
"""Remove a PID from tracking after it exits."""
|
|
166
|
+
if not _PURPLE_LAB_PIDS_FILE.exists():
|
|
167
|
+
return
|
|
168
|
+
try:
|
|
169
|
+
pids = json.loads(_PURPLE_LAB_PIDS_FILE.read_text())
|
|
170
|
+
pids = [p for p in pids if p != pid]
|
|
171
|
+
_PURPLE_LAB_PIDS_FILE.write_text(json.dumps(pids))
|
|
172
|
+
except (json.JSONDecodeError, OSError):
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _get_tracked_child_pids() -> list[int]:
|
|
177
|
+
"""Get all PIDs started by Purple Lab."""
|
|
178
|
+
if not _PURPLE_LAB_PIDS_FILE.exists():
|
|
179
|
+
return []
|
|
180
|
+
try:
|
|
181
|
+
return json.loads(_PURPLE_LAB_PIDS_FILE.read_text())
|
|
182
|
+
except (json.JSONDecodeError, OSError):
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _clear_tracked_pids() -> None:
|
|
187
|
+
"""Clear all tracked PIDs."""
|
|
188
|
+
try:
|
|
189
|
+
_PURPLE_LAB_PIDS_FILE.unlink(missing_ok=True)
|
|
190
|
+
except OSError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
152
193
|
# ---------------------------------------------------------------------------
|
|
153
194
|
# Request / Response models
|
|
154
195
|
# ---------------------------------------------------------------------------
|
|
@@ -203,6 +244,11 @@ class ChatRequest(BaseModel):
|
|
|
203
244
|
message: str
|
|
204
245
|
mode: str = "quick" # "quick" or "standard"
|
|
205
246
|
|
|
247
|
+
|
|
248
|
+
class SecretRequest(BaseModel):
|
|
249
|
+
key: str
|
|
250
|
+
value: str
|
|
251
|
+
|
|
206
252
|
# ---------------------------------------------------------------------------
|
|
207
253
|
# Helpers
|
|
208
254
|
# ---------------------------------------------------------------------------
|
|
@@ -310,7 +356,7 @@ def _build_file_tree(root: Path, max_depth: int = 4, _depth: int = 0) -> list[di
|
|
|
310
356
|
|
|
311
357
|
for item in items:
|
|
312
358
|
# Skip hidden dirs and common noise
|
|
313
|
-
if item.name.startswith(".")
|
|
359
|
+
if item.name.startswith("."):
|
|
314
360
|
continue
|
|
315
361
|
if item.name in ("node_modules", "__pycache__", ".git", "venv", ".venv"):
|
|
316
362
|
continue
|
|
@@ -328,6 +374,48 @@ def _build_file_tree(root: Path, max_depth: int = 4, _depth: int = 0) -> list[di
|
|
|
328
374
|
entries.append(node)
|
|
329
375
|
return entries
|
|
330
376
|
|
|
377
|
+
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
# Secrets management (plaintext -- this is a local dev tool, not a vault)
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
_SECRETS_FILE = SCRIPT_DIR.parent / ".loki" / "purple-lab" / "secrets.json"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _load_secrets() -> dict[str, str]:
|
|
386
|
+
"""Load secrets from disk."""
|
|
387
|
+
if _SECRETS_FILE.exists():
|
|
388
|
+
try:
|
|
389
|
+
data = json.loads(_SECRETS_FILE.read_text())
|
|
390
|
+
if isinstance(data, dict):
|
|
391
|
+
return data
|
|
392
|
+
except (json.JSONDecodeError, OSError):
|
|
393
|
+
pass
|
|
394
|
+
return {}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _save_secrets(secrets: dict[str, str]) -> None:
|
|
398
|
+
"""Save secrets to disk. WARNING: stored in plaintext."""
|
|
399
|
+
_SECRETS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
400
|
+
_SECRETS_FILE.write_text(json.dumps(secrets, indent=2))
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ---------------------------------------------------------------------------
|
|
404
|
+
# Chat task tracking (non-blocking chat via polling)
|
|
405
|
+
# ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class ChatTask:
|
|
409
|
+
def __init__(self) -> None:
|
|
410
|
+
self.id = str(uuid.uuid4())[:8]
|
|
411
|
+
self.output_lines: list[str] = []
|
|
412
|
+
self.complete = False
|
|
413
|
+
self.returncode: int = -1
|
|
414
|
+
self.files_changed: list[str] = []
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
_chat_tasks: dict[str, ChatTask] = {}
|
|
418
|
+
|
|
331
419
|
# ---------------------------------------------------------------------------
|
|
332
420
|
# API endpoints
|
|
333
421
|
# ---------------------------------------------------------------------------
|
|
@@ -378,6 +466,10 @@ async def start_session(req: StartRequest) -> JSONResponse:
|
|
|
378
466
|
]
|
|
379
467
|
|
|
380
468
|
try:
|
|
469
|
+
# Load secrets and inject as env vars
|
|
470
|
+
build_env = {**os.environ, "LOKI_DIR": os.path.join(project_dir, ".loki")}
|
|
471
|
+
build_env.update(_load_secrets())
|
|
472
|
+
|
|
381
473
|
proc = subprocess.Popen(
|
|
382
474
|
cmd,
|
|
383
475
|
stdout=subprocess.PIPE,
|
|
@@ -385,7 +477,7 @@ async def start_session(req: StartRequest) -> JSONResponse:
|
|
|
385
477
|
stdin=subprocess.DEVNULL,
|
|
386
478
|
text=True,
|
|
387
479
|
cwd=project_dir,
|
|
388
|
-
env=
|
|
480
|
+
env=build_env,
|
|
389
481
|
**({"start_new_session": True} if sys.platform != "win32"
|
|
390
482
|
else {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP}),
|
|
391
483
|
)
|
|
@@ -409,6 +501,9 @@ async def start_session(req: StartRequest) -> JSONResponse:
|
|
|
409
501
|
session.project_dir = project_dir
|
|
410
502
|
session.start_time = time.time()
|
|
411
503
|
|
|
504
|
+
# Track this PID so loki web stop knows it's ours
|
|
505
|
+
_track_child_pid(proc.pid)
|
|
506
|
+
|
|
412
507
|
# Start background output reader
|
|
413
508
|
session._reader_task = asyncio.create_task(_read_process_output())
|
|
414
509
|
|
|
@@ -485,7 +580,7 @@ async def stop_session() -> JSONResponse:
|
|
|
485
580
|
# Kill any orphaned loki-run processes for this project
|
|
486
581
|
if session.project_dir:
|
|
487
582
|
await asyncio.get_running_loop().run_in_executor(
|
|
488
|
-
None,
|
|
583
|
+
None, _kill_tracked_child_processes
|
|
489
584
|
)
|
|
490
585
|
|
|
491
586
|
await _broadcast({"type": "session_end", "data": {"message": "Session stopped by user"}})
|
|
@@ -1102,6 +1197,14 @@ def _infer_session_status(entry: Path) -> str:
|
|
|
1102
1197
|
st = json.load(f)
|
|
1103
1198
|
phase = st.get("phase", "")
|
|
1104
1199
|
if phase and phase != "idle":
|
|
1200
|
+
# Verify the session is actually still running by checking
|
|
1201
|
+
# if session.json was modified recently (within last 5 min)
|
|
1202
|
+
try:
|
|
1203
|
+
mtime = state_file.stat().st_mtime
|
|
1204
|
+
if time.time() - mtime > 300: # 5 minutes stale
|
|
1205
|
+
return "completed" # Process died, mark as completed
|
|
1206
|
+
except OSError:
|
|
1207
|
+
pass
|
|
1105
1208
|
return phase
|
|
1106
1209
|
except (json.JSONDecodeError, OSError):
|
|
1107
1210
|
pass
|
|
@@ -1116,7 +1219,16 @@ def _infer_session_status(entry: Path) -> str:
|
|
|
1116
1219
|
if st.get("completed") or st.get("status") == "completed":
|
|
1117
1220
|
return "completed"
|
|
1118
1221
|
if st.get("status"):
|
|
1119
|
-
|
|
1222
|
+
status_val = st["status"]
|
|
1223
|
+
# If status indicates active work, verify freshness
|
|
1224
|
+
if status_val in ("running", "in_progress", "planning"):
|
|
1225
|
+
try:
|
|
1226
|
+
mtime = sf.stat().st_mtime
|
|
1227
|
+
if time.time() - mtime > 300: # 5 minutes stale
|
|
1228
|
+
return "completed"
|
|
1229
|
+
except OSError:
|
|
1230
|
+
pass
|
|
1231
|
+
return status_val
|
|
1120
1232
|
except (json.JSONDecodeError, OSError):
|
|
1121
1233
|
pass
|
|
1122
1234
|
|
|
@@ -1558,44 +1670,61 @@ async def onboard_session(req: OnboardRequest) -> JSONResponse:
|
|
|
1558
1670
|
|
|
1559
1671
|
@app.post("/api/sessions/{session_id}/chat")
|
|
1560
1672
|
async def chat_session(session_id: str, req: ChatRequest) -> JSONResponse:
|
|
1561
|
-
"""
|
|
1562
|
-
import re
|
|
1673
|
+
"""Start a chat command (non-blocking). Returns task_id for polling."""
|
|
1563
1674
|
if not re.match(r"^[a-zA-Z0-9._-]+$", session_id):
|
|
1564
1675
|
return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
|
|
1565
|
-
|
|
1566
|
-
# Find project directory
|
|
1567
1676
|
target = _find_session_dir(session_id)
|
|
1568
|
-
|
|
1569
1677
|
if target is None:
|
|
1570
1678
|
return JSONResponse(status_code=404, content={"error": "Session not found"})
|
|
1571
1679
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
cmd_args = ["quick", req.message]
|
|
1575
|
-
else:
|
|
1576
|
-
cmd_args = ["start", "--provider", "claude", str(target / "PRD.md")]
|
|
1577
|
-
|
|
1578
|
-
rc, output = await asyncio.get_running_loop().run_in_executor(
|
|
1579
|
-
None, lambda: _run_loki_cmd(cmd_args, cwd=str(target), timeout=300)
|
|
1580
|
-
)
|
|
1680
|
+
task = ChatTask()
|
|
1681
|
+
_chat_tasks[task.id] = task
|
|
1581
1682
|
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
["
|
|
1588
|
-
|
|
1683
|
+
async def run_chat() -> None:
|
|
1684
|
+
loop = asyncio.get_running_loop()
|
|
1685
|
+
if req.mode == "quick":
|
|
1686
|
+
cmd_args = ["quick", req.message]
|
|
1687
|
+
else:
|
|
1688
|
+
cmd_args = ["start", "--provider", "claude", str(target / "PRD.md")]
|
|
1689
|
+
rc, output = await loop.run_in_executor(
|
|
1690
|
+
None, lambda: _run_loki_cmd(cmd_args, cwd=str(target), timeout=300)
|
|
1589
1691
|
)
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1692
|
+
task.output_lines = output.splitlines()
|
|
1693
|
+
task.returncode = rc
|
|
1694
|
+
# Detect changed files
|
|
1695
|
+
try:
|
|
1696
|
+
import subprocess as _sp
|
|
1697
|
+
result = _sp.run(
|
|
1698
|
+
["git", "diff", "--name-only", "HEAD~1"],
|
|
1699
|
+
cwd=str(target), capture_output=True, text=True, timeout=10
|
|
1700
|
+
)
|
|
1701
|
+
if result.returncode == 0:
|
|
1702
|
+
task.files_changed = [f for f in result.stdout.strip().splitlines() if f]
|
|
1703
|
+
except Exception:
|
|
1704
|
+
pass
|
|
1705
|
+
task.complete = True
|
|
1706
|
+
|
|
1707
|
+
asyncio.create_task(run_chat())
|
|
1594
1708
|
|
|
1595
1709
|
return JSONResponse(content={
|
|
1596
|
-
"
|
|
1597
|
-
"
|
|
1598
|
-
|
|
1710
|
+
"task_id": task.id,
|
|
1711
|
+
"status": "running",
|
|
1712
|
+
})
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
@app.get("/api/sessions/{session_id}/chat/{task_id}")
|
|
1716
|
+
async def get_chat_status(session_id: str, task_id: str) -> JSONResponse:
|
|
1717
|
+
"""Poll chat task status and get partial output."""
|
|
1718
|
+
task = _chat_tasks.get(task_id)
|
|
1719
|
+
if task is None:
|
|
1720
|
+
return JSONResponse(status_code=404, content={"error": "Task not found"})
|
|
1721
|
+
return JSONResponse(content={
|
|
1722
|
+
"task_id": task.id,
|
|
1723
|
+
"status": "complete" if task.complete else "running",
|
|
1724
|
+
"output_lines": task.output_lines,
|
|
1725
|
+
"returncode": task.returncode,
|
|
1726
|
+
"files_changed": task.files_changed,
|
|
1727
|
+
"complete": task.complete,
|
|
1599
1728
|
})
|
|
1600
1729
|
|
|
1601
1730
|
|
|
@@ -1659,6 +1788,167 @@ async def export_session(session_id: str) -> JSONResponse:
|
|
|
1659
1788
|
return JSONResponse(content={"output": output, "returncode": rc})
|
|
1660
1789
|
|
|
1661
1790
|
|
|
1791
|
+
# ---------------------------------------------------------------------------
|
|
1792
|
+
# Secrets management endpoints
|
|
1793
|
+
# ---------------------------------------------------------------------------
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
@app.get("/api/secrets")
|
|
1797
|
+
async def get_secrets() -> JSONResponse:
|
|
1798
|
+
"""List secret keys (values masked)."""
|
|
1799
|
+
secrets = _load_secrets()
|
|
1800
|
+
masked = {k: "***" for k in secrets}
|
|
1801
|
+
return JSONResponse(content=masked)
|
|
1802
|
+
|
|
1803
|
+
|
|
1804
|
+
@app.post("/api/secrets")
|
|
1805
|
+
async def set_secret(req: SecretRequest) -> JSONResponse:
|
|
1806
|
+
"""Set or update a secret."""
|
|
1807
|
+
if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', req.key):
|
|
1808
|
+
return JSONResponse(status_code=400, content={"error": "Invalid key. Use ENV_VAR style names."})
|
|
1809
|
+
secrets = _load_secrets()
|
|
1810
|
+
secrets[req.key] = req.value
|
|
1811
|
+
_save_secrets(secrets)
|
|
1812
|
+
return JSONResponse(content={"set": True, "key": req.key})
|
|
1813
|
+
|
|
1814
|
+
|
|
1815
|
+
@app.delete("/api/secrets/{key}")
|
|
1816
|
+
async def delete_secret(key: str) -> JSONResponse:
|
|
1817
|
+
"""Delete a secret."""
|
|
1818
|
+
secrets = _load_secrets()
|
|
1819
|
+
if key not in secrets:
|
|
1820
|
+
return JSONResponse(status_code=404, content={"error": "Secret not found"})
|
|
1821
|
+
del secrets[key]
|
|
1822
|
+
_save_secrets(secrets)
|
|
1823
|
+
return JSONResponse(content={"deleted": True, "key": key})
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
# ---------------------------------------------------------------------------
|
|
1827
|
+
# Preview info (smart project type detection)
|
|
1828
|
+
# ---------------------------------------------------------------------------
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
@app.get("/api/sessions/{session_id}/preview-info")
|
|
1832
|
+
async def get_preview_info(session_id: str) -> JSONResponse:
|
|
1833
|
+
"""Detect project type and determine the best preview strategy."""
|
|
1834
|
+
if not re.match(r"^[a-zA-Z0-9._-]+$", session_id):
|
|
1835
|
+
return JSONResponse(status_code=400, content={"error": "Invalid session ID"})
|
|
1836
|
+
target = _find_session_dir(session_id)
|
|
1837
|
+
if target is None:
|
|
1838
|
+
return JSONResponse(status_code=404, content={"error": "Session not found"})
|
|
1839
|
+
|
|
1840
|
+
info: dict = {
|
|
1841
|
+
"type": "unknown",
|
|
1842
|
+
"preview_url": None,
|
|
1843
|
+
"entry_file": None,
|
|
1844
|
+
"dev_command": None,
|
|
1845
|
+
"port": None,
|
|
1846
|
+
"description": "No preview available",
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
# Detect project type from files
|
|
1850
|
+
files = {f.name for f in target.iterdir() if f.is_file()} if target.is_dir() else set()
|
|
1851
|
+
has_package_json = "package.json" in files
|
|
1852
|
+
has_index_html = (
|
|
1853
|
+
"index.html" in files
|
|
1854
|
+
or (target / "public" / "index.html").exists()
|
|
1855
|
+
or (target / "src" / "index.html").exists()
|
|
1856
|
+
)
|
|
1857
|
+
has_pyproject = "pyproject.toml" in files or "setup.py" in files or "requirements.txt" in files
|
|
1858
|
+
has_go_mod = "go.mod" in files
|
|
1859
|
+
has_cargo = "Cargo.toml" in files
|
|
1860
|
+
has_dockerfile = "Dockerfile" in files or "docker-compose.yml" in files
|
|
1861
|
+
|
|
1862
|
+
# Read package.json for more info
|
|
1863
|
+
pkg_scripts: dict = {}
|
|
1864
|
+
pkg_deps: dict = {}
|
|
1865
|
+
if has_package_json:
|
|
1866
|
+
try:
|
|
1867
|
+
pkg = json.loads((target / "package.json").read_text())
|
|
1868
|
+
pkg_scripts = pkg.get("scripts", {})
|
|
1869
|
+
pkg_deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
1870
|
+
except (json.JSONDecodeError, OSError):
|
|
1871
|
+
pass
|
|
1872
|
+
|
|
1873
|
+
# Determine project type and preview strategy
|
|
1874
|
+
is_react = "react" in pkg_deps or "next" in pkg_deps or "vite" in pkg_deps
|
|
1875
|
+
is_express = "express" in pkg_deps or "fastify" in pkg_deps or "koa" in pkg_deps or "hono" in pkg_deps
|
|
1876
|
+
is_flask = has_pyproject and any((target / f).exists() for f in ["app.py", "main.py", "server.py"])
|
|
1877
|
+
is_fastapi = has_pyproject and any(
|
|
1878
|
+
"fastapi" in (target / f).read_text(errors="replace")
|
|
1879
|
+
for f in ["app.py", "main.py", "server.py"]
|
|
1880
|
+
if (target / f).exists()
|
|
1881
|
+
)
|
|
1882
|
+
|
|
1883
|
+
if is_react or (has_package_json and has_index_html):
|
|
1884
|
+
info["type"] = "web-app"
|
|
1885
|
+
info["entry_file"] = "index.html"
|
|
1886
|
+
info["preview_url"] = f"/api/sessions/{session_id}/preview/index.html"
|
|
1887
|
+
info["dev_command"] = pkg_scripts.get("dev") or pkg_scripts.get("start")
|
|
1888
|
+
info["description"] = "Web application -- serves HTML/CSS/JS"
|
|
1889
|
+
elif is_express or (has_package_json and ("start" in pkg_scripts or "dev" in pkg_scripts) and not has_index_html):
|
|
1890
|
+
# API/server project
|
|
1891
|
+
port = 3000 # default
|
|
1892
|
+
# Try to detect port from scripts
|
|
1893
|
+
start_script = pkg_scripts.get("start", "") + pkg_scripts.get("dev", "")
|
|
1894
|
+
port_match = re.search(r"(?:PORT|port)[=: ]*(\d+)", start_script)
|
|
1895
|
+
if port_match:
|
|
1896
|
+
port = int(port_match.group(1))
|
|
1897
|
+
info["type"] = "api"
|
|
1898
|
+
info["port"] = port
|
|
1899
|
+
info["dev_command"] = pkg_scripts.get("dev") or pkg_scripts.get("start")
|
|
1900
|
+
info["description"] = f"API server -- runs on port {port}"
|
|
1901
|
+
# Check for swagger/openapi
|
|
1902
|
+
for swagger_path in ["swagger.json", "openapi.json", "docs", "api-docs"]:
|
|
1903
|
+
if (target / swagger_path).exists():
|
|
1904
|
+
info["preview_url"] = f"/api/sessions/{session_id}/preview/{swagger_path}"
|
|
1905
|
+
break
|
|
1906
|
+
elif is_fastapi or is_flask:
|
|
1907
|
+
info["type"] = "python-api"
|
|
1908
|
+
info["port"] = 8000
|
|
1909
|
+
info["dev_command"] = "uvicorn app:app --reload" if is_fastapi else "flask run"
|
|
1910
|
+
info["description"] = "Python API server"
|
|
1911
|
+
elif has_index_html:
|
|
1912
|
+
info["type"] = "static-site"
|
|
1913
|
+
info["entry_file"] = "index.html"
|
|
1914
|
+
info["preview_url"] = f"/api/sessions/{session_id}/preview/index.html"
|
|
1915
|
+
info["description"] = "Static site -- serves HTML directly"
|
|
1916
|
+
elif has_package_json and "test" in pkg_scripts:
|
|
1917
|
+
info["type"] = "library"
|
|
1918
|
+
info["dev_command"] = pkg_scripts.get("test")
|
|
1919
|
+
info["description"] = "Library/package -- run tests to verify"
|
|
1920
|
+
elif has_go_mod:
|
|
1921
|
+
info["type"] = "go-app"
|
|
1922
|
+
info["dev_command"] = "go run ."
|
|
1923
|
+
info["description"] = "Go application"
|
|
1924
|
+
elif has_cargo:
|
|
1925
|
+
info["type"] = "rust-app"
|
|
1926
|
+
info["dev_command"] = "cargo run"
|
|
1927
|
+
info["description"] = "Rust application"
|
|
1928
|
+
elif has_dockerfile:
|
|
1929
|
+
info["type"] = "containerized"
|
|
1930
|
+
info["dev_command"] = "docker compose up"
|
|
1931
|
+
info["description"] = "Containerized application"
|
|
1932
|
+
else:
|
|
1933
|
+
# Check for any README or docs
|
|
1934
|
+
for doc_file in ["README.md", "readme.md", "README.txt"]:
|
|
1935
|
+
if (target / doc_file).exists():
|
|
1936
|
+
info["type"] = "project"
|
|
1937
|
+
info["entry_file"] = doc_file
|
|
1938
|
+
info["preview_url"] = f"/api/sessions/{session_id}/preview/{doc_file}"
|
|
1939
|
+
info["description"] = "Project -- showing README"
|
|
1940
|
+
break
|
|
1941
|
+
|
|
1942
|
+
# Verify the entry file actually exists on disk before returning a preview URL
|
|
1943
|
+
if info["entry_file"]:
|
|
1944
|
+
entry_path = target / info["entry_file"]
|
|
1945
|
+
if not entry_path.exists():
|
|
1946
|
+
info["preview_url"] = None
|
|
1947
|
+
info["entry_file"] = None
|
|
1948
|
+
|
|
1949
|
+
return JSONResponse(content=info)
|
|
1950
|
+
|
|
1951
|
+
|
|
1662
1952
|
# ---------------------------------------------------------------------------
|
|
1663
1953
|
# Health check
|
|
1664
1954
|
# ---------------------------------------------------------------------------
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as p,j as t}from"./index-DvhjaUe4.js";const m={primary:"bg-[#553DE9] text-white hover:bg-[#4432c4] shadow-button rounded-btn",secondary:"border border-[#553DE9] text-[#553DE9] hover:bg-[#E8E4FD] bg-transparent rounded-btn",ghost:"text-[#36342E] hover:bg-[#F8F4F0] rounded-btn",danger:"bg-[#C45B5B]/10 text-[#C45B5B] border border-[#C45B5B]/20 hover:bg-[#C45B5B]/20 rounded-btn"},b={sm:"px-3 py-1.5 text-xs",md:"px-4 py-2 text-sm",lg:"px-6 py-3 text-base"},u={sm:14,md:16,lg:18};function h({size:e}){return t.jsxs("svg",{className:"animate-spin",width:e,height:e,viewBox:"0 0 24 24",fill:"none",children:[t.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),t.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"})]})}const B=p.forwardRef(({variant:e="primary",size:o="md",icon:n,iconRight:i,loading:r=!1,disabled:a,className:c="",children:l,...x},d)=>{const s=u[o];return t.jsxs("button",{ref:d,disabled:a||r,className:["inline-flex items-center justify-center gap-2 font-medium transition-colors",m[e],b[o],(a||r)&&"opacity-60 cursor-not-allowed",c].filter(Boolean).join(" "),...x,children:[r?t.jsx(h,{size:s}):n?t.jsx(n,{size:s}):null,l,i&&!r&&t.jsx(i,{size:s})]})});B.displayName="Button";export{B};
|