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.
Files changed (26) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/loki +30 -18
  4. package/dashboard/__init__.py +1 -1
  5. package/docs/INSTALLATION.md +1 -1
  6. package/mcp/__init__.py +1 -1
  7. package/package.json +1 -1
  8. package/web-app/dist/assets/{Badge-DTFBYZoA.js → Badge-ClncXa1x.js} +1 -1
  9. package/web-app/dist/assets/Button-CUOgrX10.js +6 -0
  10. package/web-app/dist/assets/{Card-CL6xo_RX.js → Card-2UWDXe0P.js} +1 -1
  11. package/web-app/dist/assets/{HomePage-1Ccs1i0m.js → HomePage-C4WxoEKI.js} +1 -1
  12. package/web-app/dist/assets/ProjectPage-Dy7ONtf_.js +162 -0
  13. package/web-app/dist/assets/ProjectsPage-CIDkRHyZ.js +6 -0
  14. package/web-app/dist/assets/{SettingsPage-CULNMl6W.js → SettingsPage-5AxjoTTg.js} +1 -1
  15. package/web-app/dist/assets/{TemplatesPage-DdrBEy9w.js → TemplatesPage-DPlaAtAk.js} +1 -1
  16. package/web-app/dist/assets/{TerminalOutput-C3fQIXqq.js → TerminalOutput-Do2PhilR.js} +1 -1
  17. package/web-app/dist/assets/{clock-D5NTAxeP.js → clock-DpWpY1Zx.js} +1 -1
  18. package/web-app/dist/assets/{external-link-CSO4H-LC.js → external-link-KmF9dPsz.js} +1 -1
  19. package/web-app/dist/assets/{index-DvhjaUe4.js → index-ACgjqVp2.js} +17 -17
  20. package/web-app/dist/assets/index-BZpAV5M8.css +1 -0
  21. package/web-app/dist/index.html +2 -2
  22. package/web-app/server.py +353 -63
  23. package/web-app/dist/assets/Button-NSmP6YCA.js +0 -1
  24. package/web-app/dist/assets/ProjectPage-wby1itP1.js +0 -141
  25. package/web-app/dist/assets/ProjectsPage-DEP-ZiW7.js +0 -11
  26. 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))}}
@@ -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-DvhjaUe4.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-BLBd5LBk.css">
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 _kill_orphan_loki_processes(project_dir: str = "") -> None:
113
- """Kill any orphaned loki-run processes."""
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
- patterns: list[str] = []
116
- if project_dir:
117
- patterns.append(f"loki-run.*{project_dir}")
118
- patterns.append("loki-run-")
117
+ tracked = _get_tracked_child_pids()
118
+ if not tracked:
119
+ return
119
120
 
120
- killed_pids: set[int] = set()
121
- for pattern in patterns:
121
+ for pid in tracked:
122
122
  try:
123
- result = _sp.run(
124
- ["pgrep", "-f", pattern],
125
- capture_output=True, text=True, timeout=5,
126
- )
127
- if result.returncode == 0:
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 any survivors
140
- if killed_pids:
141
- import time as _time
142
- _time.sleep(2)
143
- for pid in killed_pids:
144
- try:
145
- os.kill(pid, signal.SIGKILL)
146
- except (ProcessLookupError, PermissionError, OSError):
147
- pass
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(".") and item.name not in (".loki",):
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={**os.environ, "LOKI_DIR": os.path.join(project_dir, ".loki")},
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, _kill_orphan_loki_processes, session.project_dir
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
- return st["status"]
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
- """Run iterative chat command on a project."""
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
- # Build command based on mode
1573
- if req.mode == "quick":
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
- # Detect changed files
1583
- files_changed: list[str] = []
1584
- try:
1585
- import subprocess as _sp
1586
- result = _sp.run(
1587
- ["git", "diff", "--name-only", "HEAD~1"],
1588
- cwd=str(target), capture_output=True, text=True, timeout=10
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
- if result.returncode == 0:
1591
- files_changed = [f for f in result.stdout.strip().splitlines() if f]
1592
- except Exception:
1593
- pass
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
- "output": output,
1597
- "files_changed": files_changed,
1598
- "returncode": rc,
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};