From d08f922631f08620e47928ac9a983304dd0928c5 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Wed, 10 Dec 2025 21:51:00 -0500 Subject: [PATCH] feat: implement running agents view and enhance auto mode functionality - Added a new `RunningAgentsView` component to display currently active agents working on features. - Implemented auto-refresh functionality for the running agents list every 2 seconds. - Enhanced the auto mode service to support project-specific operations, including starting and stopping auto mode for individual projects. - Updated IPC handlers to manage auto mode status and running agents more effectively. - Introduced audio settings to mute notifications when agents complete tasks. - Refactored existing components to accommodate new features and improve overall user experience. --- .../feature.json | 14 - ...h4_Screenshot_2025-12-10_at_5.33.08_PM.png | Bin 38219 -> 0 bytes ...60_Screenshot_2025-12-10_at_6.09.19_PM.png | Bin 37452 -> 0 bytes app/electron/auto-mode-service.js | 328 +++++++++++------ app/electron/main.js | 69 +++- app/electron/preload.js | 24 +- app/electron/services/claude-cli-detector.js | 24 ++ .../services/feature-suggestions-service.js | 234 ++++++++---- app/electron/services/model-provider.js | 2 +- app/src/app/page.tsx | 3 + app/src/components/layout/sidebar.tsx | 122 ++++++- app/src/components/ui/dialog.tsx | 2 +- app/src/components/ui/hotkey-button.tsx | 42 ++- app/src/components/views/agent-view.tsx | 4 +- app/src/components/views/analysis-view.tsx | 22 +- app/src/components/views/board-view.tsx | 60 ++- .../views/feature-suggestions-dialog.tsx | 164 ++++++--- app/src/components/views/kanban-card.tsx | 27 +- .../components/views/running-agents-view.tsx | 210 +++++++++++ app/src/components/views/settings-view.tsx | 54 +++ app/src/hooks/use-auto-mode.ts | 75 +++- app/src/lib/electron.ts | 342 +++++++++++++----- app/src/store/app-store.ts | 15 +- app/src/types/electron.d.ts | 18 +- 24 files changed, 1450 insertions(+), 405 deletions(-) delete mode 100644 .automaker/features/feature-1765414180387-4zcc7wpdv/feature.json delete mode 100644 .automaker/images/1765405989164-sxpyqufh4_Screenshot_2025-12-10_at_5.33.08_PM.png delete mode 100644 .automaker/images/1765408161734-damvl2960_Screenshot_2025-12-10_at_6.09.19_PM.png create mode 100644 app/src/components/views/running-agents-view.tsx diff --git a/.automaker/features/feature-1765414180387-4zcc7wpdv/feature.json b/.automaker/features/feature-1765414180387-4zcc7wpdv/feature.json deleted file mode 100644 index 44e88b55..00000000 --- a/.automaker/features/feature-1765414180387-4zcc7wpdv/feature.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "category": "Core", - "description": "do nothing, code nothing, print yolo", - "steps": [], - "status": "waiting_approval", - "images": [], - "imagePaths": [], - "skipTests": true, - "model": "opus", - "thinkingLevel": "none", - "id": "feature-1765414180387-4zcc7wpdv", - "startedAt": "2025-12-11T00:49:41.713Z", - "summary": "No code changes required. Feature requested 'do nothing, code nothing, print yolo' - completed as specified. YOLO!" -} \ No newline at end of file diff --git a/.automaker/images/1765405989164-sxpyqufh4_Screenshot_2025-12-10_at_5.33.08_PM.png b/.automaker/images/1765405989164-sxpyqufh4_Screenshot_2025-12-10_at_5.33.08_PM.png deleted file mode 100644 index c383cdfe6ae9a8adc576ac712423efe72661e53c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38219 zcmaI71yCG8^9PD+aCe6g9D=(;kPrxPNYLPRxVt-qKyX5^;0}jta0vttc0eGwyS=^q z?W^~y-WAL3?auV{boWfp{JJMvLtPOIgB$}64h~CMNlp_E4#5!)4&Dz91$g3NBuEVh z_d?u8R#rnPCRhNw46 z2x0i`wXb8EjKdxx(jSL>#-is*z^K+InI`zl} z;-x6Mv|T>FfS<#6S(XDy%wMwj!-(vXNm8XugM|`BFP~KhpE+;6r=@A;YBcMa@}2AW z``TB=a!5J*-beY$lf9e5wUd639{TcIHcqpyC_fs*cP2`=J3<(-6<74UB^ZiJAx+6X z5*wp&MwZcQg53{}{Hhfeo{bcBNoVx(C0dN`lKm&uIITOGeLg1pv`>Hr6s@`O7`}*}AM;@1AXX(703km3B-Q_hat4nuE}GO3 z(-@bu5F~n~O8X{Gch3)(1f`#KdxOx~C~lfskYp=-$jdeA17^%ds_SW?41|v8&Z`*0 z%`A<_VwBy775lC~lnW6r2nzuoQOR^1jzAh4*W*IqqBk%m*`ym8KM5JB``z%138l2e zC=&{tn4Uabbg7=7?Qpl>;RvOl_HoOtsicej8k8g$NW_ALXu?9pZj4{$F_E98AyLaJ z+~6D=g1os`#XuID&$%=oyZG3Lly9L(#`+!fkUodJ$fj7^rquFCbu^85w{(Uf;})Qfrc_vfnozWmK}M0!<$V~O;0 zaru7rkqQAu*d|sr1Pj6kXmfC+Ak4X_Msr0AFKbWnwBssiC+9OS zm;8hinkSU%3icG_iN*=>31dR@@DSEItJfxP3E!ro)z@K?vEL}*atqPt{sKqD*2hx7 z)m_JKoL(~PJgrK({qAx-e}d~Su`n@NKk?+OKo0V!KDlzbn^@$9mdg?a)vJk>7Ao~X z>muOt@b^yC`QVtl5f*wcA#k4SSP#;;Xoyp^@P$FA+;{enNhTBCbnNXQd<~Se;MuPT z_DBZZ*fkUfs95W0Lh$OLnCtiul%wvIUsy+~Sx3b7{y_L99!88BzFZu<757wcK@z+Pv?)N+&#$LGgr;}2| z4rtb@s)mgBzo~VpATRIBgt~WzxSMt?C}v0faz;5q^8~-ljXFP3zESn#F$zBJ-ai$1 zz<7c??(4#{Mq&s-?1}BBi=+F9$4u}B?N69Pm~5EViP0W&I|F`ns~p8Q)OHfBDB*sa z{vZ99{o?(b{puSGW|GlzytKkeeo9k}oTW+cW%L!74oE zY3{u&dFjdO^j=$gTw7e*Tsyq1UVFamNL#%8PU@0DJJ$BCh1Rd{ntS+r9(&Gv9D5Yx z@s?`mB}LO1HY(Qq4Ji%x4LG)L{4D$lVk{ST^w*sqsv|;yyNQH!bSB!fA5|BVVNWL8ivHWH#eC<796}X~r&KWaT#v znU|aIUt=8nm^#dpLao(WT3!0nE!8G=*EIq=^v95Sg+&9;-B{e1%y^+?*0{)+MAq+g zmdr{1HS>VU5VM|!p$3q}lSrgG>j7_RTOdfIjp%! z`wIslfgb{m0-g>wO#w|Q_T>)MOL&Lr9xWdGhn$NASB_VUSE*MdR2rzBsQG<9krU7` zXl|c;UwPyjxKN~yGM3VgibiNxL|$mz-4^U4-0LE@7xjl>BJO7UU^{!KefD-jsWRD= zVJE?!`gZBZYWR-ku8TXN!~2u(dIjW)9WL%*B8VfnV^_g5*3DpjZtrS#rSHRr(a!3o z>2C7TyFbpMrQ>m?oR+zlj9w^S6K7%@w^O4lJZc!fltN#EnSNiK*9u|WhNT>Ts$^@mP1WURgZp8cetG{;WV>f6OUN6W&S zBUZ;s#HmILBsHa>i?&XXW9XyTRalL$i@}%YmkYI4zZm)J%Ok*(Vun44Z*FeJW}ep{ zAD2b#$(&0qNf%)C&=wXHR)p?}??6x#U8%gMY@^Jte4K)qqRZ$RPsU?8kdDb3!P@(I z;Jv1;=3djxQ?0bQZsMW3s|uTPo5Y0W@@MCr0g8l4ia@uJJEWs9#NDuqt&5ypW60F9 ztz#exOTnvx#KG-KXvg~fx$dbil0*nmNJ)l71{-gl#qGz+vAyA|QO?ODqPlRRUR~jQ zVG7sP9fcjoNva{0iQg&JpYn3o7^m_7#4Isqs%G=nY8}vW;(4l_Wj6R|<}n)9Rl>27 z>iz_M$CBLoUDPyfT!<6n7yBw!LRCG}&m?A>aO(3_>6I3Vn!i4!;luCOe&kO2`)>8S zuEmn+J(;!M%)ET&miz3ndgoT|lh_#@=GYeMN7-vTB9k9@t`FO{yvNVucP4Y6|9(j_ zwpE#V!^k8^?8UVJ!8LwqxKL|7^UXcBGxbbm@{917)Gz*eV+MXt^T}ORkM1VQ+n*AO zj#LEM88`_z+B%n=ZWD+y*8sqob z=Q#|ni|E@n^ev?Rlh;0;KL;#qy({0b;B=raK2HE$aAldYuBCISwbc*PS!F7X z#UQ0A_cs34U|gWj!QywPBZBPWy#b;QKNNbLz9*6Qk6;@qNm`T@SMYH zL0gGE*8KYV zpQa^!zZ27`O2x`t-FA6KYHtTo^kWR}5@QpubB9Duzq`xq=cJgc8#%-}>>cc` z|6-Y}nP|wq6!zW9o^L5WA3Mh)S>~E)Zfm5uTRFUzr8=UzBoXHnFgk0WSr%?RU$E#J zWvN0k682AZz3grLZkIDpP`%ck>`}1K{JdRn;N5Jr!rA(GJ8{q!)>d@=`Y3fxu+{i6 za)rjXb-67;0?qH|6ZaD|N3mUzi<(62XsJZ?q+u@AF8Wc+h2O#X5y=Kg zp0CQ&%tQTN(cx%l;O+D5s$g;Ted^Y1_x=y37pwMx`*%%^lEu$YZ4aJ#YhMDmZ)!Ff z&NnmO4(H_tiLZ(rURQOI|-Cgthp24*$f;0`?Oz79eFyH8r?b zz&#oq5!$J={KnE-7xx&E_ zGr=x+WlhEtfc~7#dypGQO;yC)$$``Kqm!8hrVOxbQSRu zr~g+%1h|Ji=Ax(lSH#UuoF1g6K`ZOzVnHjw$<4`4FM&ZzODpE`(NaWHPT{|+1HZ)S zt=-(fB3xXao}QeZe4I`$R$M&7!opnKyj;Az96$*US8qo*Q!frjSBC#0@;`LsEL_c9 zY`|_dPL8xNx~67M?r!4r^st8h_w!$TT6o#~UrUaz|J^O1gIusTTs)lIT>ncOs450~ zDxzWIWnr%;XX60K3}{1wPl#LWU-|#foBwO^KWc*huciPm?|;_(k2nACn%b@wF0xJz zK$C6~|EI41uKb@b|GT0X7p&+1A&LJI^S`HnoFy>Cxc*mX5*UnG!b^Y}DQ)E5z6ZVm zBm4L11pdDSF4#A4$*~<(pU%U=0Nx%1kKgdDN{;24FG`W@SIlZ0j+1NIMu8c&- z$cTq5vqj5GdeSEl5Re#|_h_FI$FmS5_X(Fa7?+j~KIUcQxbP0Qs42_N$%L->tgGm_ z+tKe|#*Z4;hrVo2<%>_-?{l>D=y{8ud6MDKli<)HIo+w7RDMf`R2I)lK}h)Wv?^@H zU=_j-b1DdoQV6GvQc&WznH8}})1TnN4fNG1MAZ-&lai5MF7vu930vcX@xOvyr`P%a z(aL}e&&slj^k)bz#Qh3Z!LSsNfe~@01&E+VRfHf-X8}ZfCTV=%0z{NyL~NRbb@0*N z1od*fBwl5N$ESrSfl-7O)~GNh86prm4*waS0~k}H^#OiN)tf8hr!xZ;i-+Z0NCDbp zQb1Ki<_uZJ;9+zPh}uNtTVVH{nUCZE9lbCLXfvj}UJsy+AC(UL=pOIsy6j0)zCv{&iNkIktchVRbg1G@I;pj1o|JwWmP|=h?K7d0n%mgZySp^-Y0AiDZ z0MC1we)utRJHQ(KUHYdRC|^tkkhBpjs>QaRC?_xaN^h(#=BjWviPf*4!)iRO!5&XC z_r}RJFWU!dZ@8x_&m?@iSob%=n^!=D8I?xlRY9M5>0gEA--X~bi39QKrrCHlqN`p0SiIu_1U9gfnZ zA}1qSD#g~1lO3p=npUq~l^W@=syOC4Wv;reN$(0Rr^Yz;cc@`BLl`W=frc29Xc0IG z9PK3A4P(kME$qkUKP9Jz=cPr@1bo-RmhgHXaj(eic=n1D=(&vf80!#EFBWn0W0yF6 zfowrOU&#~!M)T`Tc8^p*Q+?==_mG}fi2HGB;j_q^5+(0Pwm+j)P%jEEPOnMOiUzhO zZj8&s{1Oc}$d)R8$4}^J^IGrV6(H9L0wn_Ic6L!&5z)XNJe%QuuB*$sr(#Z`Hx&aQ zTT=L9Q)i%^?AIO{Ks&PNh56t<)!7_f223N#LDi)u(Q74RyDm=zyZQGjx67I~i{9!! zlUK@z`hJ`#?Vk)6PdEJ~Mr(*LM+$A_GmJ(4`mqLlpN63nWb z!f0myBWSMO_`bYmf@?G-ctYAna)EW+;CG>mv-eMNM^dN9JMwy~S{w6NtEz2QT?_DE zoWa6-x9Y<>(xbZl8M&I_Gxx-lJ!p>lJS@COG4i9 zO+peoQlP~L%k1YJeQzZ0tO(LHcDg*$;{HzWCQIgU`-fRT+$?KfxUJt$F@shWN7yD( z2V(aN-w(oFUER<97CWVCy4;|0|tG9s5UDIa$%%7wnCZobV+)0z~_xYz2C+A`@A z4MX`bxZ%Skx7-oW6`2~DDCY=&J_ZtAhVeOi`tUt0Zr zlcn(#ZmMhVC&6*uDV<&G$-a6OD#}a&fzgeuZB!waneRV8JPdfhauJQY$@O;bXl{3H zp={Uf^o`ys2TRxa4_y8o`hIZxM>;sjo!M#yR}Hf&r!ukLZv^`qYZUu~VZCaoZWV6yVv$}=PK>7!HFIjJC`aon`)n0r z@HJpTk;_-I%Pq;-v`j6e<$A{x5Z$T$H$Lkx`7jxW$CPRXHk``7aGr9t?7iEfD!2-) z@l{HY@R+_p_pi{N61gr@ICM@sXtnA}{2F{>_}FQo0N3*>issbOLd(#Y{c_({3_(M1 zRTd<-p)Ims=ablX2rwyX(%r=8pt%S%3gT(MckW?dm+>Zu! zxSC(iM^On?cE@;m)L-f8^}Nxq|EAd&H?u1-Xu5b=k0_C*YjABc|5e}Md@C+yaPGs- z>&c)+Heszwqndttx#5Yn>&QRW+^OKcbUKu`V*UdcM8qO!w;5k%D1wD&`0Oh(Zwe5I z`@Ur&5Evn){l;#jlnGiwI<^(sr!@^Q?U7%p5&CwFd$8Vqts7~NcP{akBM^(iHWiu_ z5c#%zr(FN;eVg|8RIh{pE^d;W1d*MP-f>hqNx)Ah1;wHl)`I(RHrnUMcqQjRoQ=aV znRFkhm!4Amc?)|7BxfrtlS9h1C=q1QXu$zx-<840|fVu)A#sXLD874feJ8tmY zi}fpA^Yu@YdK)3qpT}Fjtws&-wZ2F1*-#x_ZTcBS^YBwtzR)x7ZnOPS(HA+SJHw=> zPjjV|Bx~zNtE3~5xKp|^&dwG|r)EV`(xf7BHS%u16RV{(nI~{(a|nKJN@Uz(vWkHTdiceMfWAz z8s#e!S^UZd&bIfiIr*Vhfcwu&;SAyY>1_7VeE z4U`+h{MkpXDHapkb>Al^a;6rhOm&N32j_+^yEY$J`nBdKWs&*TJB za8&2k)*lUsaL-9565zO}Bc#YN1s}e1bXg?VW;)zmVRO0M%dWO}Xk&|0Lf-!!C*44} z@1M9o!617NZN3#1?Pwu3lALF+r1pRK*gah|{;@A3jBVMzMdR3m%Xr_>z4zwvos)p* zfW`!w&c~)*qvl5(%2QxUnSbDL7C>Z|vXk7xGlgOZ9@rS1F~?7BTk+JnZ}^|8ofleq zrzV2B{n@HeL{L7CUVCinNQFRppBgkCPoqj_nMse>)|QWmc$yqRJw~_JhteFjd^*}> zu_S4>wf(}{7>W>wyNLuiI`Qkwi~(L!`&;sj$6l@q{ta=^qK3cz^RV`(>a_k!&jSG+ zv8}9pmFL(CUy(&G;`V?hN2YWrA4vlw{M*?5znNM9!G?W#4_NE>G`bd6SFVA0*!9_L znF&AwT&uM+=;ewDB2-&tL?}AwXw2*dBFOnGTFsbgFILzH**RLBbH_mMh?;(kmta3$ zhW$Vq1^rb~mu`B!$9xtNgRprJ1Ths@a2U@Rt50hE=iKfL-3`#G4516(Q|M zlS*`@rASQ4<~erENc=ZX7Bwu6a~4<%Vur~ZFb#4??i)x?=@wDs?3hjn)0=~}^R+~q zyu9Z{{_VEr-8Rus{U}o^_EN4-hVJ&=o`iX|*I2{5+2)lgAQuILo==bF-NGb0ks_%qBoeDa!pRwAirK@7#^{#Y>3>Xq`n#6fI1wZFY|m zUJ(uSIiyH~g$3kH1hC)cHjbKx3X**M4SKo?-;>sJ4B>75D8hIv5T~@ z;fy11nW|`QC!gbdoXh(9Kl&S;2Z$G|a1>4WyEHip86ULUbsbhsidIzaOIEFZwe=4J zXT4(5g7g%)J=s}iJ%VonHoQ34SvT9@M^|mB_$o@&p#oZ6Y_3)IpNly-nAw$UPP;{7 zB=xghrJ=-egvYQAQV<1tVJSEi8Fd3w(oi&@Zhq%rx-^VJj7{+G6K!E->pM#4$!v5L z&6D!d?xp#uu zTfndTR#9mLw{>8^v%yikW2D)J zldfsXX<%8gZeWT~$eO?aNyrDDh#M3+A0}l3I}<~pA(LCl1sc+Gqsn|4w>8F3T5`WO zER#;c|IPP7QkiIi4>z(kbRI+kx|^sN&SfFtz3FdeOonksgKNpyB%sXmgjXLajH|X9T zM!~L%Hc=)kxLWK$j{Soft>2p#RxA_Hv0Gq+bJf9)@uvj(b_HYz}h_7dB^A`IGMknF&SUkV&>!2UOF}+oJ zhu5`BOJYyQ!+?XFKjN2b?oa8Ies+;4)KqpgJ3z~=F)cannUuJz;wD08- zsmsF{>Q6!=Q%Kdzzc5!g_JmQ42pGM$O0blX0%hUiUj1YkRDaofXrpQ7>^-hI`fvUGTzlo5_6-x_heBk0W}QP+NebM!rmpK$h?V@Q5?f zOmo_{?xEUP_fzZK^mj(Z-|XDH#bGxUa zv?4Y5)7cJc+2#SXGDF`B`adEPKVbz>nJ zzFK~m>NZ12Mu}A(O>iBEftw;?4`*0DF&Eg`Q@~O%a1fJ!hqKW{2jYAR6X5got{8~(C4;LH{l6_Bl#dXm>n4N$GDJMTpT)-_mN~WD_a6~up*7PDZ9AJ?EMTYanfK+ zIWRGunMu-4g#1{{`YRa5xi{$j{Yf$8cHaM_$9zDPzzmI8h7rkH;`AURY4r2Veuz|HBkBZ}58`};fy8?Ww0B;1*stDtNezhG1maPffA#-qmx?u~ z!2Qmebl*?*0?PyYhr?LUQsRy{DBJmkUe{AH?m8IPRCZxAqQg<$$sVH_jgD6o=9dfO z0H9{!Z>?X;UK5igtLx6neY=i;L9pjowugSsxlrqR@RDpF z|BBEwkWv$}W27*{&8MhJOAt;$JKk!z*{~Z3(g`=0X+DC^bTN;03O<|$J0R4cY({Z` zGLv5}zd-4Pt?8x60hlb-`qMg7bV1gh;9sLs=VKKEc!X{0sb7ftvPe6Pw>PF8knY7}4 z1!p<(jhq4!RW$}U{N?E=%!`kf0@y+N*YtmF;>&+^yo`%3yUgy@*tK^r-6;7y$Kn9T^KbD)l@O}vq@dfGr6!613%sd8M2?4wA_#awFTkM`5n_`z z&X`;MqKrv&FrGht6IKQKdkupvkC$t#7}o^=CLFWyt|9@9Y7=1EV)?UcHzO`TAj}~kfvcWyBj0&eS=WSysi%= z#!yXr)vWhr)|V#F>niSg$pKiN|CijCl>w{CIqx+}1{QE~?8|kob#dW#_A|~wMoGy( zY@IPRi&}`vfzFH)lg&QI|5YJmWfN7YbFKgAX00l$Izej&Ev`?_N#Q8yo4VwPf!3e_ z%E1Iw*3x1%eyr!`t`IVy|`Kj0L9KL zb`#DMDK8%22X~KfT(H~7ti8B?D}<4~vh>QFAtjdjmr~Lf>oAG|O#sM4-eZS@hQBU5 zye{vPy52RSS~E#XV|FyPX#%i0!ts}cXqt={0T3sUPu5-Gj%s!3q!_q8jA5B}W@J0f zdhN4=a@Foy)De1^xxBGTSLyF}Ok4mGUJQ)FW*ebWN_=$2D5*4zNS?rbt=SC>)>1BB zwWcGRFB?lT)k){aih=iE8xHKxS2=U^=uW^QGtu*;#li}hTXMbf}_mTv~5$s_~i{Y~S67|WB+5a5Tx^vBO$Rd-|Qc;;R0 zFF#@HLqmF?;{OTVCm|u|@#*L$Hp;jOr9Ec%Iw)1XiBc5!ihzZHd{q`whG5=N_h|Qf zp0i{8ahstZgWdJyRk*cG@3is!a5SmqO3BO{!5^CCtfX5^_)wJ7=vrD+FGQ-)fsu$m zt1?uGcec4U#+j+XU4O95zkkr;KvN4rV!;IMM)*V&eHELW2^Ler1v$KO$)N|$h&h&eGg;DHWnXm2RVa@xn{h~EQ_j81t|8K|do;K8NVUr|W6C;p z5+Mzb?0H&ySV@@9iw&jnHL5nh;!?F0vZ3cD*GaFP?9TBlYX)X3v*?5u1WQV_5Dz@4 zbJm42z@u!#DR=IUpxH3D^nb)rKW6Ut^sCi(31?b!jD zD656ZNO316T=n=(sA>&Ol}&w&NF(Hy1w>+}>$@>7eUTGzTVaKmZJDhK>G`J9(Ql4* zFemPEh#Gn^*~JsU43D3l2257Pe;Qmg7?W@j6d?KA#T=EZ5G!?H$|Wu|QRFurb}5ed z6C{s0n#7WKjy0NW3!U>atrT1}IKL98AESsfDHY*(&v}vkbG^YYb%tc0cmKx!gPFr3 zPi^94S8fK$JlQDeQQs-ToN(cA7)6@&{=9D(7rPA*>=0Kex_HTda3k}dK4}5XE1V*2 zkJb>_7;r4$7dT8~5Zj+f?vlLaDC$+4-KrQRZyzlzF%qM2aqaqL7SUm+W}rWI0v>po zrkQLK9?H-8zE?u%Ktoe-RU^7b!|oH=)FNuUu-#7ZzPw=3#- zAdJqKC8gu+fcL`Er)uZxfd2Ub^o=KU``Ft@voA3$hT=pKhm`W&?QZCkR*jl_}kZGrxBMRSSkTE;U-h1`wn z_qWpy`-F72{HxB}FM5ptvT~ zOx~&k&aM-QR}YdpLp*$+*DeplqW%aT`^#ZJ-CEzNGx}0{BJfRCe_`u#OD?^hH1^Y9 z!fqPuh*x3@d$)tN);d7wd$dpS=f<^!zLLK9K^f0+;P+Y4&(YN6xbC$~;=PLLJqz+G zxwj%xdp}97KagW`fLxCFTt&=;IxL~?zZ+_q{z?d!q`p-gyoBOJ(9FmJn zfj1YMrm5z|W3)9lt+y_t9Zs)9AE&ghM|Q_gr|@fkX4SUqI|lB4D2`ccf5uz%e+~wf z#X{`oDpd9d75L#QwbJ|hHX?4!!A3RcDoLQ2IDP;a!ZjgrUP!9h?ecE(@%6aPggzo( z2sqeYagr1mu6Xl*k__l4K{(h5L$(^3_O}^IDIw-%d}bO4Mv9mYX1Q2z-8twB>wX2r ze52j$uJjl6WH2w{#vfQTSXB~Xmwox?b=Gmaj;*d&Q?`kR(ZeLi*}|7xbh~e7{KbC^ zj$4dI$W^wkY5Zr1B*#qM#jyB@bO?u7n!4sM9u})R=((N-x4#y6O)@woFQxLk94u;` zl^f7tWU}Z9vMwViQ|ur;iyNNeI$D>#3pbd!8AcEa&VTzBwOc|2l<*CUnW73Td;jNr z#4Rs#=K=*Z{!^lzpAJHMV3J&K^`)s)zI=Z28}cLw>9le5>!Xl&ex-3lYb2BLW^;b~ zN}?sgh3ydTj#2Ayn1N{##;CRqC?O*S59~)eA^F3&y2xiQ%-u^WTMP-cDNQUc9yVU@ zaFtY`#rymu{(d=z=cy`ce~VVrBpwB)HVcWO!C4aZf1i@qJ$KFrCCo(bz6AQb%e|fd z66$f+H@Wi4z1(hVNQ-4Fmx2cEfBI*&s|f}DUd5)NBjSSU6z_KvwGOXIGM!gw?tmWd zO(jJUkC%L}sCr+hq+MCF`L5k!zl#Am7bIwrocQrVBX*z{STZDZ3K2ejmEkWZVzDdgaMZGQQ?ETlq9ex&a0Yy4H3-2PVrh=14h^OnKX**f zM~f`Te_`e3Ef880u$$COyHoFD?+F2c4q5T<^?r^S`bRD=xD1-bNgA~}^lCj5{z(sC zNfGLC`wWrdyzfOmE8%*QZ+de3qQ`o(v6z^p>~s1%>rZW0pg;vWTPa%_Mc1?bTj39_ zq7OIDPQnNNY#ND=h&VLK>%)_<|)mq*BO~Gb4vkY-tCdw{Pf_1C7Q}{UY+S+E9UuK zc7p0uj?L@BDK{;xV`uBqxa&>M<*ScZKjK4}?6b4od|Xq80bFvWZh!vX-=}`>P{m>x z3)3y5T_MH}qsZGc%k*X>&2(&#{1%Nz0AWgr6aI^#jHynwwIG~Cd1uSgf#e>V!4LI|bp^KiE>w`2Y z4wAnQbF#ZcF6inXT6YtuJ{f#wuyZni`bS_s+~3y=O3=EO+iopYcaH3kNi@*xer{U@ zcRd9k?Pq)ZSTlO5`!FuwvuU+AGTfKvFVnOlK80g&inZn)f6LRHcPN^`@$fiEooYY> z8D4IE7RSM?NSbKaOS5qKg=1v(f^^~@g3_1Qex*BIV&L4XmK+BUztIKWxJuG4a6ntf z5x-Y2HQp4S8k9UwdEW63MrFbeTOXJ7>tNTOB4&3Nyd3dw79)iizYC09yIGk>8Y`$o zG&?imh&)bJQ-^x2iPG?H)cK_yzoG6#?YbYzi~rm7=fJmNna*tTd5GK~{<>qwzWU+l zD!N5I+BM{g*`{|~)Wrz4yN5&fLI(!#xktbQLhg;R{ayMk;jpHSP%l=3o{;u-i<3RC zvg)t)6tIB8HT!}4mRXo{M~RThqS?G)80GOIhuf@obAOP z#l+!lnj>xmrwNPrI;gB&TnaMfQfK1{_O3$?uI3j%k5HcpBg>JTccaS*zE|YsYX>Ui z+GCZ3pv`rY;4QA@?GD?f_VcH#v4CCKMDHcBlHir8?bJZe+FG3)uhY0Amizd`(Ckw7 zA(cY&?oP+V@K?sG#c6_!Xj@cW8b+DwP9kiGNT+#TKc`3W^ri}S# z>5G`ayue7ZNCz(cqx4CN&U0HWwf!fL#iw&|7lq0YSNG1jzce`98{L|5a!^a||3ZCv zV_~=gaWnMEwBI7WO(ax;p?Eox=N@6#feE-cEt$wm5?0RAiNq-#NJ7Ju6_dOabM#xOE^G;&-IQ`^IQS>-) z<4{5B8)qQ&s&wU;1QcE4=QF8>b7Nnb;c8RRzJKi`@C`d(0~g<-jBuP(!>auX^5L7x z9FgltToG>Xct+LjS&_@`&ZOWJW%fq1rA=xE!It>R5MIkQ6^_pi=5^XyyV9!5JO!Bb zy7N(5_+*e_dx>>f-NF7-TeYrpxyp<{<#YEdCslqi(W^1#d!=ts?8qNn+3hRrM13wd zeQbfOD^@=3?i)tboS5jTQ4;j zbN^0b8>JtQZDY`noBXA z;yb3eThGa3opma%S=KGPo;&>+BFGhwU10OrycY8%>#DyinRaGBc{sKZpfBF*BcI1= z#BBJg`+?ckX#YkN)+2-1P)tG42>IN6rbTtg8A6Xyjf%u(?S}YseEeDemps8sr<iqAy9Jv?i##$-oC=3Qu1AUWQ`lq*WX6s0mF+5*< z=UeT?y6=E42Om4Vv&eU5)B|5q_MGudbFDsz_Dpk6HyZn<8iMcj$^KG)uAY4AY$2J5 z)q*@;EE$hRlRRWgy#L`>x~WxMRx+#Hj}nlO*2YKsy28P#NdVQ&bx|#LtJ2fAB9zb( z-peZ#1|k(p0ssMPSe6Kj*x4+)TW(t3_-S zt@}6`+o6&rfioy66#g5SHj)OVx3zk_I zRQC-}joWQ0+qF0hREQ%#R3N6iH}20aMIvPEf_Mz}c@Jwjy@ySatM=_m&S&uIQfPw0 z;RfGR2s&rTUM_$8ld#!Xk@Td$d5vUL@id$Ym3j~zo`eCCQV7uiJe4K}y(;VK0*Sxj zdY!V^P#}-}xcePFJXqWg9MWCK^hpcC(P5hJsTIh*Pkq+sd%7)Z6seL|G~IY?WGcq$ z{M<})m$KNOXlK>i`HDiBc0sd@kE^PH>>r~Y_t55{XYhtGWa53()rO@C8q$21*iO3O3)tB(fn(lMK>`Aw%AcB}j=Pql9+%?w4Kb{&Q#^t*V91PErm2z{-Z=Q! zlVqbLWfKchQe7sKIPzbIa$aVzBzMU1p)}y)KqHD46P^4McTZA`y`^)171U>=J73v@ z2ur}B;U}$5h8A@JS{;(1;S#|?KLXJfE#A__!1(Mi{gC5#!j=0N*A;NxH*h~IEMaJT zQR;rS6xi9jTns)h>H8;P0G6!)^U2Dy$Kd|MO^1Kc@fMzV(Dwz?CK!mNy}@(ba8;DB ze~B!5Hr(O~u&uEk`nF4!e4?D+g65&$oLLU3A9=w4*4TJ`r6S=4FrjNu2Kt{BY+vMd&1v0+Y_`5Vr)LYQYPz{D z4)oRH+6g=Tgmq}=Vor{-9hLutn^Fq*Gs)>n)zg{9Ds3Miw9}5wCko^V%$wg^lRg^* z6Ta8VfjdEC%NkDEBNhG-RN(=1?eM{GpZVMC&y}-LXVIHaLHacmDTkc;?JdoK&Gn6{b z5swpzFT#S@ELNlTt!x$wt<>6Mi z<<6b2dX*O}#!Y(11+ZghDOa)+U4+&M)v@@}w1*G5d^Z9g^+Vy0A0O$mR@yfeX^6kN z9~&3R)OO#$$htJ1;nNp^xozKk zj~YdX_L7p0XdHSrfzd}DTL1Hg|7wR0nPSq5*xaI*_hlM4;wP6Lqw3@yJoA<~##iin z73ur6)21yxnosapiUADzM>Y80^3qw@(E}Z!vtlXCT{8)m$V4*Km`8go=BZV*wWQeD|7?J)BR+x#rL`SXP zL_*HS4_a?ilJ$vTHfw|)yl2K&A_P2@T(U(6Z0g5#0_Md#1c zp3lBCbK*`MPl-1fNE@)M@+u)cGT2g_F$oABAV4E@`NgpD)RCMAM=s!Qn-CAD3n}*7 zLbIjGjpE1T5t4yeQfwTF>%S!LJy^UcLEL~D{tLjX>#$r61*~OmCYZy-<3U7M^#pkM zeU&;UUqN!kY`gbYQX^!au+0}LM21&Hj5xE=_i_s0XRENj%!!$yM=H^4X&qU9yy zdH_)3D39SP38peJKw&p(w83#eC@k&N7#;yumIB5IpZ38^bVeAb--hapU^Jox0UBNe z7XFM$9zeBe$B_k7p!0FC0AH=vWN=ClaA&D}k^>{WtPb#c$k+drEY%B>0woUxK!P?{ z2qt9$B^_CWU7$A{{yPccK$vytHPB>+z$PUZIuamnOsU>~;$GJT9&_f9dc-1qf_2in zViEse1WlovqnAUn$+YI;H3PTM_`v2F6o}9Gg)E4}7(D@WaQNCUIjY2q4v7{ z4#@Z4>^FpIE(vxJp)?QisvD+?So%^R>ZJT*d;n(rv?#zko^JCgFIdD_aOz~Q0F0SP z;QFuOJwOIU0oa-gp^^2NnhUK%6bh(*OC{j*p5z7`fQIaN#6KDeFd7J|#~Xka#RE_G z#KR5RmFlSfsqi&y2oQLnT;C-idId|$-8&0BKJ-XE7H6E9E2HLt0iC+PN$}S+0|1p{pYT{en}P$?gQs+IBXAK%dXGd z5@{U;Vur{3|CBEU_a)J-=H8T8AoN5a!F!Ic9p_AM&V0P`%?F<&e)^V3=*WsNY59+L zGk|wIhAeetAmZ}NUO|!=qxtV*E#p6H2$x#fBO|zsi&rze|BzGZc)9GobSzybx$R4C z*koc+XE5>iik`^P@KsK(F$r)IM!1@i2$qomj7^n-;Wwtb40w*}QHSM zQX7nBq2Q1F|CG-G&$hgj=p~SJKeNf*jpaz181^ZY6(dDFD~913 zq9*@Rc#?JbQ(s2qKLsiQJkw2`1cyoq^>R>}601`y)~~8y2y(@IHUh>6p(Mq>lnda* zKmcYh9c}`+Pp=5tZ*xxKBzE>Hn+&W-jUnhchUg2+8R!Ablh^Bvfz$`mi=`bwU^pKm zxK#p148KqUjDhFv0TV2O4cNXwJd0q0ET$slM{P?`8RR{A5O87@a8n^U@t**z>P7;F z6*_S{h8i%0UT3NLeDdh!5M@yrP>>&Bj&}d7OfXanlq-a7l`iu7KE?sX$Ud*EMYM%w zT@^!`l+IMn8LJs}8QD~P)UTDCT)XJ>40Rb(V@E9PKdS0qH;kI>eEkA`rkK{rWAQq)c<@%bAVOa!9JExBS4|#9>S5?r( zjVglD9n#$#P`W`vx;rHVq`SM3lsYs>cXtbjG)Q+G>F%z(56>IVz4!eK?)`y}u$?`7 zX4aZDE52*ZF8_?R&2(iJxqC0e4*RF{`opv^cU8_-F@~+g6f+?3mAC&jrg;kqO86## z?b8;``iUQN?Hv2~^IGt<;1X|*-yC9rOI4kV@;W3-a6M7GfR2Rk{g9oI2MuO_AV=!V z7+5n}({bwYyH=@qj9z%O%0RZnyO03Rhav#LrdZ&dBV_=7(KeJilIm>!sJVaSva6Au zFsNGJq$|BAki(SFi>%(;^d>EKfPi91IaU zWj!aeTiYZ;Iv@#`$V1x5k_sSFO^fOOSByj zRlUqDX6~DN-IaN3H4z!k-~tNQi+Eq9pE$qrGc%LQ-BKFD4?@8VQhIKFy(_r9zYZ5} zR#l}2Iz6x)0u3vXZ_u+X3H?L49(S+PxziTV&o`-+$!{86=Lz3bE!_SF@U8c6?!OJz zwgBeAWAsWUCVVzF`>zOFBb~V&kBx65({F`orTNE4AJkpN;(4F_p7q3k|2&@Ahi z#si)Jn70`3_A%S|F(gpb*)0)}PTp~Cn{9*tz=B>@b^mXSA5eYgUYVnujdJEEw*FKKy z3hQw7TRi;wt@2K#v!zepvk6dfCBvkcNGT1yPQpxHjygL)hzHAr-!4HBOB}Tj92<1j zs<3&$h^wN>aMbLuB55-os8%ONDkDbpw=e%$dkfG&8b~BN$H+l|8TXZ%XzoJ+3u(T3 zRH^Gmkx6sjR!hqtSHg(_EiS^!^l%yu)R~IsW27`~cdnx4)HrmxMK+Pi{6;V2U@Vn& zhGuFtG%l-;yf^`pfqcrvgs|kUU_gi|v(f8A-d2y7=Je1M+Bv@MsvkS#1EU$*i|z?d zGaiQW9p-*_Rdd$F40c8=epD=+Kj)17L>1dTLVxqH-Z?M$i+9e>FrykXx~Pf&T)B33 zFT~r8+QZ_Fa;ZZAxADu|-JnVrpbx~$0^(*MPhcF3VWT-C#>BOK_EV)PV|m5`EJ+(K zr^}(Q$;fk?*={#tF;LUxKCIyH*i~CMrbJ zf2uBDEd!NboyTm|5Wz~QC>n$JEiH>m7!)=^g$@UB1l`YS$w8?4V~SW~p{^R$wXj8s z*+ai160)r*v+$n?Jo)+VoPl+z-l4siAD-zX`1_;Ndynp*v@fKh2r?JhKbMR#escs- z|Hj8``vysmE!5BF?9#zC%SUxWE$$KTbS2I79(`F^_$1)qZ^}y^lY3cN!j^VM85D`R z-mNs?h!30QJkQ}(LHC>`TN0t(e(URtrk)^`VCQ)CQFS$7+0~==pS!$I1Bu-i!>g_+ z3D7A$XVFY^g|Wci#V-?8(hm>+WdQ^ty-Z1YO$EJZ;ldPf?5EwC>8T$d-VWfUM)nn7 zhpbb4Dc5qgs22%Hq7#YaR>=uU^CiuAH_5liHH%6Rs22!htNjeSPbd;-zz?_SzG7mu zkv9Cjr;`L4<|lg%RWRcuS3IAVi0CzwK|DA&p+dLjdB(DDQKJUes(-DfY+Tc{;LjlM_T} zVcVv8&rMQ6nb23FU`BD{M{NN6mU$}KNfO=Dj8p!1PsC3KQ@|$aSA;T5+RZ^6(}3la z&@T=5D*@Wd%{3587IFNFbr0(T)>H^)+YcG&`Cv+6gljWB#E`EEQj{e27<_G%4kyDg z#0Xc2OLqJ2xVK#lk;{xUFqrYveKpl@yTCU=D5IWt`L(Yh@QVL6Q>#veWh_` zaY`N^mXIlidke{zEyF`UDKeM)d1|H);3fZIsNI!*J8Ibvj*Fm8AC8-}a4WyCnR#_> z-e4sS!ChJU!>2LajY7=t6ClZuhFZxGYF9r!%U z#Y^A5Vl-<{J};v>L$ojT+6QIa=PbrTlwR;{N)og@MJrpIF92IjfE~bMMyrgrneewk z;051G2|OB|x_R;v)6%x_N}*1<%1Iy~aqyMqTEGU7lHvIl;|%6>XK!2UMg8&U%QUa5 z+2?SaT=?15fVM*^9|rHv5D*v8TankH>2R^6?kCzku~P*;R6^3fE+%n| z`HwU~0Ol_FO+1TBxG7(guDM(&HZI>TB0{bXeKIT}HCFFv(o1BINNPbqf_ap@3gE?z(&=Wl` z1Q1&eO`((tXk_XruU@^`ht>k<{(g>I`SKFa*Br~>ZV<0cH8{sEB9Uz9#YqtfjK>J} zz3FLj5GGh6Vm8z@M}+@hh1_)3htR|tE5K6yqyPaE99aR0*?zEXKH=kjsF24N>C=d=qv}&+U+Rv9PHO*`T8)Q zn_udiLsyn!6Wn&(PAj&}cbefls4``M2+UaJRrut3agg#aN5n>{0hK1Xa9Tq&`X$T| zFwV<(-Jt&DW(+b`FMJ6 z&4T}J#{S2#q8rp(fN68;BHy0@;fZi+qIN^SE55^moWx~jbc1&L<$&l$F=SNzMZt4` z(TK*&?gcrxXj2enV_R~ght+qRczFCperS`=dupeg=rsHKFGN3hk~9m3`5xHrsR-G2 zGAOPM(on-W7HY?(r{s!pi_n8#wFM73Mt*Z-$G6+Xwgc$+69#V?FqcNY0w-GtpUw*~ zxgR(a#7BpfC@{0JcVjgmVVsLNj4KqoF`)edSEz0TBw1NB)vCR?833fS&c-kiH70d} z$oB>aZcOg?=NkGJ*wYYdH8YEAN3fls<2rz(wJ-%J&#^QQ- z7E7xy$-Y%|C(>g}!nS!5yWx2caIt}^Tg;0G2I>pz4?ojQ({4$?Y+Fu~ldQ5`P@| z+!mxm_nI^1TP-n~8ZKbnF;RCY0AOTmR$i|c^VbHDo4Au!zeRx@J$>jB+$uW1!0EF9 zbpGO3r<=%^(LvR)g848tpp|ZH7$={ffwNMR7O&U;v6Xle9G3QuEPKufDh2pyj#v41 z)Q+q;`@guG0*Wl>VUzg&Em)aCjIagA+LuUFdKd8PSeyXPjsF*g2(lCC#zSnxCrSJ4 zlzpTC($fRf{C|-?PlP@;2W2_DPAEFN>0cZ`h)IA={^yqZUmukWQqB6ujPK5m!YkHEwWyXT|!_IgYxHP`= zXo(CJ!98C7*8XpW;AMh1E9MZZhTYGZ$IYXj9R<}m)Ud0y| z0$!$F4L>x%O^MC{a7sWb16MY?D1Ee!YbtW#xA+Z;l!ZSmRL7B{J{s6Iu_-{Ah<-b> z$`{R$8{pJmDOwg^t&*jcmjbieo1CnlLCtkfA-J{>T6u)cD9Xf!>FKXHsZ<;xA1YfJ2_XmT z7-3y42?nQ`F%Ltx=<9oc*^xZtS*PTw=^dAeTD-yepTp<)1h|H)V(Q!pj3^v49}KNOifJdM>KI(WIZt$^hGTWK6X0Pqevr}~Ss z4G(o-;yW>=70|~mnUCr86I|z#!#9+++78q|j@R>Bc(WsNF-D!}sWb)0aCpAFvj%v! z;QC_fMWL2m9`~b7WcP@zD$`3D5DdVFfeS$}xK;&Ji))uc7U-)R>(@~^K$cVewlWIR9c3+1kfxc zIQ-d?3 z2QC+uxF!qWT>bygdpWvI`3hP345~$B!H76<){6}T1#)Q{d()+#UP-m6Sxyy^j|p6t zd0kG=*0`M-=?KyV>wGN$S zk5`Ll1GHJ;J&+IQ?&K2<);hPReX*==C+laE3POPA{QuO~;!?9?p2y`LtLK$f3afiK zk)VTEI4+3d&0FNZxJ{6r&>^-lenRdTD&4?t5Q2iukC@L8Tq)dji{WSei=bAbTKsjT&0V=b)|_6eCLvwWdtujy$v4|BVsrlb@c`SVy4tuuW4qyaa3K21Gy7MRCg*%t2IdM_ zxO}#F_cRg2a9F2s>ytA*@ z>cVF+Q?@xZGZ6pGJyWKYdoP^rxIN0#al1uWE1UZ{ia3c`Z{iY-n0KSe>$avtU8XUb zNvkGx^RMsLuojU@f$aEo;(E~fFaI}N&JnnvjKP@_HJw#LPOIc2Qg1r@!t;|P2PQj) zm@A>1wKsj`I*mjUF(l4RUpkiTuMV{Ie5Q)Vvz9# zCj#J*>elEKfBsY$SE@Fm1}V#D@NRyaDNA8aqm+=BI9_Qd9u(OEidnR)c*AprWhXLf z@B-x9a;gLfr%m-qak@fJVs$8mEopt(b#>DC?%|vzeGmZ59r;y9Bnd}A7yE$@o&v$w?60Z-kD<#b8?8yJjRfN?p7a+xQ}v}&ES;LCu2c>ctY zR-PZXRyJ$Rpxd~u^pOyj!I?H#%@Z%TIJ*ifiu8Z_8X}YDz8BgJGPTvX1^8f z<5>sjOBQzwus7Gk8bFRe)fUqXAo&wK*pUzvLLv)D;jz1}WT64n;vhBD9+YC)>hjf6Mv`RqkA=4<`JA;Xg(Y+B#$<~C%7V|ZU zHrt)gkCpCY!02#u(@zm1NR?MRyhl8%;>aJjg~rm{>ClwIw+E6~I#X=nxip_6IitqJ zk??k1%5wfT;f*Q)NAeEsk$uDxCWj}~@8JInUJOFQwf_c8r=-j4dc@?sH-%x+iy4HV zgec~oAV^91Ig&jhnvhE!5JD8v{Wmz<*4v{Q7SHqK?WCht8&|ymqvnZ6;Ffgm<-q1n z9wzB~D7854=3M7nuXczICb6){Brz))a(^D^BlSH{<#gUt&G}s@o58Dgce!7AoXDUi ztzMy<{YOwF)8`Jq@C;x-@OZAo01`_Zodvzx>QI(YCqNnP1nwz_lIh5uxXYo#%B&f? z(Sg0^?WTq*Q)6YM)#L4yio#%NWk-n?vu4%bU48COyBHmA^VQvm?n6{0{g(i0JAzoV zU2V5q5_1M7Nyf$q*J?r}KAZIU7HAmdI*rpByZ(!8FnY0J`M*noZtLR}fr&bEwno-> zs0T^N_^eHvI&8p*3t{KSCTh(TZvRC9iQ-EYjkc>5LHeK!fXNhu9#!={4}IlL2-&}p z2|e?x>3f}07u_aM;J5w$YG)@h z{+T~+hMj7dDpcoj|8M2VpNeWJGSv5WGieIp^%(uf^NJ#GBVNy^2Y2N%&5xVoT61(; z=aWfx_lJ%7$`!giO80$B3zA@_l!lwb`C5soKfcr?X-H+>RX?Nf*=CI0(=UEr&Fy+3 zVAy=l1t_4rQg^*4+!|*8PC=`lOCI3&oCw7x{Jfi8KtCBtgzto!bAEaVV%c_{hxHoy zid`aS{lEMj4i`AMYN~|gQrQQjn%bYAJkL&g&>|d^(KnTLRcU3wOmU1F%=3-5S*Wf4 zX&pg}00&T6*rs69E3>a~+w(YX{Q#)FmoLX;|0gK`!)B)8{_2o&5MM;_{=l&vHXI1u zsf)C^Q2!eF6T4(J+;RJK3JB7*V?OlYQH092v+xzk7=_uWbGx_6oYpNJl~;8s*Q8!m zNuz0OX3_E6d6-twBWYawcBCij91CpS{_j`Iv)y$C(ch1l!(xj4Tv1=Ew@#yMy84%${LMzZLi%8iSF6jRbEovH)a$95LebuAN3yB! zAK!X$S}&+31s9ga+H%wW%AM+G6U_3y;?dJT#B*wS2gE5&5_)LwEG7%eRrE7>or!@T zhr!yJ)9chxgjjodzp(XZ_5r`+SF5i(-i)810;|iYU6*X!7fr1Fjf|*&h^4cv$D{3P z!4egxPJMeUgKrj;xiI?$ZTcF0ANk*Q6xRy-Q0L{$qI^zIl3gIYQZ$Aix&E0H!_LtMBrd+eSTM9i#_h&Z>0JOD4#TC`08GqXyX|>oO z*O$On79RH4;Bjd|OglypkGlEhQ55hrJpLB2sX*#`032+>FfP>S!JbP%;rcwuY}6zc z1~|kGyr5OPcH2-vIm@e>yxHL$cq@-B2SwvI?XJ*v1AYaZtpeL4RjI8A}SO$s_IPQWTR%^g@%1e{gpsDPW|{}V46 zut7{IS&5{n5OgEHZdcE81?wD)@MI+eXbf$rFJ^z3`p_P*vz&eYZ|2DX#ZrJ9tU9o0 z@X(xUy9doQ=*o6OF(xmj&KzJ*rODBT^gwNv*Ia<%Wh0sV38osFbUkC5J_gjsL~jSI zoE+C9ZU;GT9ML{?eA{6EhwE6gq zi2e$g+$r%3pynGi`UYa_wT~0d0jxn8_BtzI`hKt?Q1kD%@H`QM7=RRN+(~1LhAxXb z@MiDc@OiV;F93{T`ak@SbAU4{ZG<+ghtmHy6bW<_IsXQ#pa((ylHa&%a4^H5cmpMV zcOqT@C=dzt7*!)|ugCfE0cM;l&F~B6&J)1h)NZ%gsZyasaV&;GBNZ=`p#%Q-1Ti)N zDhdE=6W7gjh7UY93IzJL8fMFigY^Q|=KOVNBeeA|b)bzE2Qd*cf>;31f~u%i0lFt7 z;elBMDH;=FN-9HpnQ}Ta1lq?zQ882k?OxoNCqRyPnoxpf2z2@fKyAX7@c@PQQYxsf422_O_7+8c(Q4$RTU_^V08qre9znArSV@Kj^+d{|Lj0 zX9Ksl6}$IbLMNVzo1nU@oSJN--%8!OeH`D}u}asvb;x$^9(w`Le)omA_|XZc*#*+# zrDvX?%kM+aKw7i$D4svL1KyhJtl*ZGKjyLYiP?YyIA^!k_GRIM{N#dl2J7ha4y#O+ zTSTkfvP^q_aMWy!sQqi&3~RLd33sMCM8DaZG%<}@!OM_FV}(?Q07eTCp81zz^6Pk+ zTD)YVW@j3w=4`z);2Y?)1iY`Ve5O8@&v)ZFtj_bW{jqVBbwipZ#(J+jYjY_Z>CB`# z_FohJo$E}wu@Hbid+_^B@gf6|U^oGQ!?85IfLUEq8^=E%_^#chuJan#floZXJ2_uL zv0A`T$X&OGzELcF^sLnPXmNq0rr?D~oC!!vEoRF(tL#fy$7;oZ;KggLdK6%m3+v;% zqt1p#_8T{9jfDC)C$AO=P5D2kRi;S>XXand?#2wqN3HWCJ$Rm(C5WJ$4_&3ST3<3@ z+x(F|1DFliDDuBTx<@n#*#3gNEfdRbb`+ZPecH;4_b4~>cNZ_I#P>FMJ=brnSsx+p zGZde`s5HSvedt2jC`s14d%W_iKr<9P^-n>tVXS7nV_d@YBtozT;&j;?nlDc1ZaJvK zNT-;I)j*s}s)93z(mHj=6U!Cy2QKe^?0UZjwSMoyd)eN>YhCJbdGSjzTb_C}3fJbJ z#I%+0{>Ozo;5fHPPgvC{Jy4h76N z7JBy$o#BhGl~IbXMNl5+gs#)p8&yP#GI^S{Gv#6#f^|Ec1oSVyQ!lOk2=uL@+90k$ zC6KjYFw0ES5a>Jyv`g)_m)I*GR#l3f<%PC&@uRGo+(Fi1p@~}{g7$|SkvT`xcl4S< zmCP{vOEuT(tvD>>1!1a3I_fn^qd7^Mkl4GiXf}s~a>$wNH>S2<>)_M%D!8v*Kg()7a_g$ik&yqWmK{BN}ZB{mk`&x7-&HQqBMT9@Zg9vuct^)R`1G zC#4Rn|3;oIZ+bh5+V^m&g-BCW*Haol;oS#tTWkfL!W!vu8qr_D-#$YmKXh?w_V2Q= zZ}pio+R7>g9&t*{R<0Mxi%$&9i$4~{eWlNoI+ket$)5~?3x9O3h{o_va>5CtIQYW0 zb>BTM<8Zm8CLlYUQO$z(Y_QYX6fr8~xz-lB|6zGDS8||1Ja4dPllsfZQk=9kp>TI= zqvw;4R{6?wU}>~l**32)-8)D`{(SmghA?mKby*o;u*}8dT&(L_*_k>ar6&f9B1a+UPcuPregk1oi6RaCWxp6Y2Si_!M!i&=ZnW&Beo6rWC@K4is&v@N2^wF7IaV;Wlx} z`?bu3?xa3WQS5e>rFGQ%-WuXBJA_+)?0VnwxFoM!@)v7mLc0E-ZKpobrjfj#;gUJ` zyznBmyxskLcEA|Cm><-06Fr%4RLMGVH}r?scRRhEH9KOPdYIwOpiD#z!H$DvsBPT5 z0yFDk_%Tek^U0QA%X_iWBohgba?BF5%&A8_`o;PnjnHH)9r=O&KUf)0#@3 zxc!gJ{YpDDX_54`3v9bAZioFtFE5Mr%)Wlx9Jf)k5YQ4Q=v3+ht@i&C;-lI5jqeZ&t*q#1(%5x#gjjvuev9MszkzueqTA~vqZx4(RCf2r)|YM(2(gE$g8Sx!&aGL zxo~{y7ssFfn&Z&WWS}~oRo-cp1FN*O(-IVP+pQVccRE3Lkg>PI-J>4#Lz|z%Pdb+*IkdV#Wp_ZXZ3Fn zw-F^C*R=qGdpND9E&`Bh=F;+)h@P}ro54P~PDf0(&$-J}m@XLg2~q4Nz1Uqhk8&py zZO>_@E!@eXk{;68UZ!pVTg9m=ER2*>$SZJ~rE662x*#3OPfH4aG^c9#m7}^6>l9h0 zJCZ##{*`I#6dYm0d*)G=$Z=1T_0ldG>{_0yVd>#QY_33jbgkQmNmKYLyRIAx^}2V& z-9qet>N2)W-GKsvOx;=u5=Sf4@4i0Q^u#f_vo3$`NOdLhDH|HYwjtnqe$hVY$P6eU z;9OnK_D+U>ALxS{i8s`5U<{qcIrS2{ zbl&Wee1UM#rR{y$@(lCF3SNoKW0J)lKa;>ZDZ$L9@$79Hhg){pv;BDnX992Q^WuF1 zbw$uqTJw#Sa7rJTiy9rFf0lcY24_4qhrtK3oTdr6pJF zba(Tgm_W+&#kZwkm69G1VB8IP)@eI9~HqqDjlQ&iio4^2E13@2InM*DUopK)d9D z!KS!otecsESx-I#4YD@EIMZS_tk$&rY|Z9Kx*EsjGb@vtxz@$A4p9@h0Huq4pc?y7 zn+z-i#8;#LILi&q1*vkCw&6O_Y5r(}Bqj^{Nx#aixRuF8iSSy;Uh# zYN&KmnTjc%KxS&@EZfJF$$RW!7RC~9ivwJiwZbRtD^#n1ka=bw&_`$B{jROo{@lc# zu&q93DT8*D98R9-ov)A)`{BiRbncN*_P_3P+nm|^8IQp;w>>1E4@{ioZbFC8X`k}j z8!!lu+snS}QUIX+yqlNQhy!V0NkEOfJsA2Pk~d1XWV4Df>bXHbgCuCKSRe|YahkZw zwFi3~^hxT)f{Pv`5pZk-61MXU5dz=ywj4F}k!us9tx>m}JcIX1|87SkvtLJe|L{zn zHTyGn&$O4b=z?RTZb`yfK>4e*5SBLVnaFbrx$(@f!?v!EVo=-nMgTP3+%S9ac&Ge0 zj>twZI zv6D~-niFv5-1wMoc!PuJtpQN*(tPZhtzVbsk?y$>M&YXh0Yan^+j*ik@o3T%n3Wz;3UU=L|}mBTMq z;{)S!HQMa%L&&P9#& z&>4S5MJJGxKQDvLZp=f!;4*ZT6k>|A0M8cC?k%Z4WbS1osc~|W#PNHmXe)m6m$WOA zYEhQ^zc;mC`wj$1K4T?K@6SdTIQEBq#>x=shoxL9n`s*=tYk2Yv2Bm;x?AmU?!nuL zP^KKNc^V}RPW%2JC^vdAtQFN#Rpj}`=u;fCr8dgM-X$2l2Hm=9&4k$@rUF%AKp+AY z{%TB*$5FfwSEiEloKQvqpl$n>6QD7K+&E@pW{z>EVTMLf{(gt~`@I-G<`t3QacrTg zda&!aYrXWs{8GEx=-1UVx*3F#R|dcJ@i0lS3%^vIM6FOnlUFaCa?@&-0IN zUzp=(`>!|?h2ot_5wy%{;(!lU( zTsyX~(-%V`C)O`2i10+K$H;Bb;4r*-;TPi8=d1Ebm%)Ifqp0s^SO)K>4 z;ptE0X2DjbR&_y$T>Cy1=8xOFy|IqOrG>{ckGgZlCMax1AX|8fjb_9~RLZH$n&@7q zQ@Bc|*)E@Q=O|0q32&#vz&pmPy!NC+%XRf%GWkWC+szgeC!O_9geMEZ?|b7YL|TQZ zVywAFtb~3<+zS@5Bx_LF%8EP=tCwc)Yhv)@eOx4~sFMU$h?P*oQ7-*O?q2i5#|)6C zOo%8{EmqJnztp1i9^$|y3{Imp8U`u^lKJO-28@{zk02aH&u}i&448vlf;T_Q!T>~*CE_Jp4ISyJ<25Aj^}Z9=}?xmct-)JFX=1wSX- z^qQqx)*uTTGab8%XE{EJmgt6pr2AO;zX8HPus5VYAPWnqp+i^xq) zZN>XEryRbbp=d^_zSxm(@2^#z0^C}KzJr4>R&D;kIbUv6_UVVnFa;Tih!oj>k6~$T z`FlS9dRU(O*mFUQ#01L96B0?jH?AwkP?Pq%$45wG!}tAnYLRT*`eX)&F7q88E9+0!p6Gjuy^QxXDB8NVbmL@9T=Xir?`}O&q$?X-LJ z{V;x&$C{xQI!Pe9>WyKLq{t5qc=Giy28HgCJ#@birME-XbF^kSX6UKtZgZ7Rj}s_3 z{jRypo=1P(>wQ5Hf`WtaxBjGGt!x@;!a4;bT2eoGm0#OMy*C!R&M@}CZb9Tq=(;Tm zlM4MPe9|q_BqLC1?*PNZi$8svEeTHslw;$#z3;It541cm>tU_naK{v$xj)s@Nvevm zRrPG%Tbds-DJL8>`;$%)#tQZ{_N6uKx%Y@YY)To6Mn${1jVdK7sl;N=Qp<@MH%hQqU^wb4hS2tU%b``fkT`k020s z0`A5i!U-5v*>-O}rE4G}1wk~|A{cxO?AKZ?AK2jE!{&4R``+B(Zz6S&wZ~60nzJR< zlpTG!Lz9c5V3w#I!^Aq+?DNmyeta)}N&;ah&3 zUn_vXwe%~cY>v&W+_uixOXTk-*p{Q~^Xt&nZ+$mnnm7mJtVAR~ggR;+ekk@?!^OX~ zJp#Fw{so~Qk(RhQhEPw}k0=V9CxrS;&xrJ#?Jl7GK1T!MpYA3Y&T_6M@{DND<2}fD zMoVZHWy%L%dIUMYAQWnq;Wu=ZhjpFxva07mZOX z{k~bE7d5RgXLwzbF-8jE=UbN9xHAEnfo1n~nDiW?D}RuxXQ2V{M~VSYEF1V)Kk8Z5 z44EBLQ#aG@GuAc;Y$LNFjz2ZpyHy=5p9_2B=~P2s&m~b4P#UKS0ZzqwG;NvvLDF>f zD+#tJ8;)u1k+wh0DfQFu9W(|Ew;(WP^%aE8Qt!6w-wtsgLOCL#(MOvY&H!{*_S>$| zIJeB?vArRa?o=f$Yb4)9;ZpQ+m|LUn$hTzo#_3OU#SYrXJ+jQ0(mC@L0R8g_>ix^_kpkuE?0v8_Qx&Mdm3`>as;r%!6n<3o%S(g0eXx$Qun>&r=!ckS}`pl zN77H^{S|>9(!_VjOY(KeRR3h7;KrZYU$nl`D6srzTf%-(z^#MPup3?hB?McR5{;5pdc@)l>ByO@l}V5Jy-( za*C!&2b1JGH8fJD>F)o&UfwGTVEbwhw(_F?Yj#EBLiUptuS4P;#%DKN+t>E(QSUra zPAl9V-vu{@H=duy1V-*r!L^AtPSsq`2X1fXAU4NoBD~c%`TLS|@*1;Bz@HA?rors} z#w*CH@xAlUEjVI5q3Z#q;>aR7eHT|-T7~CNx#?&j6=*fve^tk{DQb~5=k>CLUn6bJ zCi)~ezFSVhdUJ@qr)C$!*KSF%%nJ;7t*eOqvs4C`qX^3NY*7FnkM z)?zCzRY8?ADMY~i`0ZrY^y0)~N`6H8h*MEm)Ej%&dO|A4O7BH_bN}hs)VQIgty=T|3 z{QALLsi}7yNR8bB8R@;aXK4Yyw%x?lJRWByFh*kJ% zRq|01<3M&JT`ChQ^k%9*fS-K+49-t-Am_@3+|QcsDkAufeDbSo`E{nA;z~_+LshUx znH38(Kl+r-2mv~_s&{~g-gEek#3vXGXzuE#n}e52Ltvm7S9E4v8vi_KfR28YAq&9O z{Z~lA|9{09pwXNhVBILfvohlShyh>xydf_4TWIh9J7Do&Q4AoY00rkt$SU>2F?4Ij5X_gk(hMAAHv_O`4Om~p$;h<>04poxfC>@7MGzd_#WW~nl|^aZ_(1>0yotG%|W}Ppez8AOjZny*jTU)Zj<}_ z0jvTS8Hod37%~wk@nAETQ?*(%GA7p47jejzfN#iF+ zL!rz5l7zrxO~j561iinH&b$T${iGx`3GBO6l6AbWDs<7q&Hs@>hbRmTVBJy}=A#1e z#jXF80th1j+A6WB5dozDaG@lp7;<@;&J-{m5xIoJSm7z?xLfGg{eb8H71~4wUVMw- z1I@7YTZYcqJA_*hQ_>0g?dW9Um%}~`8kmBoN+b8*fwD`xV_z&l{@(<^hcwgd1z8Z9 zfW93?yOE*8t^%N{Txs&iq2e$QD0=n(31yXcfQ_S8ppnz>)Tl`sq=;D6rk9u@iAgmZ zR%eY+B~<(r<2t_2xUqL@=@SLqfWjin^n#$}Bty^bF869wR<3ZAYPP=(kv-rTs{G!s z14^3m%`RZ(+aG{)IQk3MxmT^|gh4ma^wWuORUOS^Eba|!!+Bki`>XvqAx17;W}?=e z**W^9hwk4ZP+m271M`t#iJ(nWW=~K>u0pSkSrifF+>#=^k&a34bN!4-==81B)4csc zrN_8p(Wr{rW(AY$0E9!rWauJe{KJMA;kp3a5!m-}TqdqilExQVhvodsEol4>(Yfb; z$BBR00Cb>Rn&Lyqj%z*)z1`dmcJY+y5hMv~EbC<*%Iv-GwI9N`dcs;D57rSG zsD9a~VnD7Y7b4eXg)&eO$hWGaPYSY?hIR}k*UKT2Yo}uSF<9r~xiFFz*cJrm{e=Jx z$X*{q=}xi4aLK@E!i%keinMLAwT?t3kz%F%&8m{lt3El0^98xeOGYP_a-TLN$*T#i z%}g)+&8!Twjk#6YuVXonHlDMrIBh!G-bI#~DV#v_9S@9f9Xb4kiqMUzJ< zU!TrT4I>MO>K8|C_<47fIQ9K4w7#F4v)`w$II2HoR>?h)G>TU+hT_}OJKw1)Yn?JK zRtM=X{2`I6de^CUey!Bx{-)3CW%siFhXMqTjnM#hH5^wOi!`0S-OLr-%3A2`DOZu+ z%&<<9Tp^ykD3_?D?%pCoL=~8p6$*RGS$xgn@Ckv*a_|RRd);GAF}BT*)zm|$_OtWO zuj7P_vW9T>JO)x+g{(6yw2c{YwnxOqBXJIT|Nd+znBUestOF6#!&MNEE3-oZ?dm|C?&EFGYK`%z{$%bXa7DJ*Lf|By^r*u;6GsNiHh-tsU$AA(=y+t`mQ*D^ zF}3&}wrTY-VN|>^p~BuV3lTomfC^30{223u(L5ddw-HfoCW2LO?@`ZPF0IEyk3tIg z+41|NQJB~J^oKewl7Cz>Xc{Y49a^KR1-aV;H_B6o4oO(6gzWbt^#pI-%ZgZc)%K#0 z&!IxXn1H7wT>4031aL`!vkZ2A|&^ai#Hx(BUtOU=xB#z?`_j$@J)JK zoF~akJG4v7ZH3lp$Im=g~gV0wiC=R4kMJ^I8~;aEzCotDh5jQb=(lY~B&*7pqx7!&&ECpjgpQbdyN=;0<#7r#p^v27CXTd^s zGRy5~lu`$Q-;sEbqS1ZOWxn1ni|F}2Z$oWHkpD~dJ>u?D1Q$!*eiI@WeA2r=4T&O` z=jOI+a`6oVHYQU1wa4k$ZCqIi7CNeD)BWYm^1ED%BMT+~pw>*Y-bTNZe$n(OuE&=o zUT5(2>oKQW)%4)8G`k@+^42`r_=(;5vM(hUpu*lmlj%M*xj#emkK4nGyS2;I-dip> zn)3#RiyvGf4zxUkC(LAMlRY&lx?BQlTDmOuQ?-2;#2BvV%4-b=7C)NB5UCw2cceDw ziQ)pNFKkk#A1MZX6kqjm*xci$WVItdS?(jI_T$s{% zJ-=s4)D&AzLqF~~)t1%#S75?di3>CwU7Ft$J3`Bgz=q8xkCf{Py5VmL#E0*GA4y?1_s1EM~hOS@pFZbXB}Om#W%APrJgy=1kXHM}FvV zrI(9X3tCcWmb4e2g>sRwxS>)`6X*gFGlE9gyTyCoep}K9!zETB9aM9=M9(C)&Q`=2 zFezu;AX?(|HSM)px9oRj%Yfjr?WrpmfA{r0Is9>}fXVkj;v+A7317#eQ?rv!k=9~Y zJr`t4d-RSN=Az4-*!g|z>9X0N4Ylk@UV>8AydIUpbh@@%%s0VLzNy#Ka7IWCk8Q=H zsgFxWT6_+d{ zl4=>S1QN-X2JRf|t?ey8kZo8RY>Hcxlm-5meDVz4Z>-|nVPo{PHTB8^s zDmnRQLHW{f-1c`ilj96_mR;$+6NmYUGN{?(DAh~JJ?N=!rq9{I1Vm7GNABOB_Ut=Y z!^J*Y6Y9B4TR%C}bPpzt%i@*gNV2palSJ3|-g3gwtp5SMn5;E(fm2wkRk=U)n6rNg zFNt6}U2xJ&f%c8YRYKi$-uR<2(dQb(Mu_y3;p)#u>>@kS&PQDSpP+P^6)ox^A??E{ zb|!zck-fb0&y8s%<(~3zHohO33i*7N*RkKXOopis=4gqH-P@UzU)CicetrXXNs^WB zr_{P#wprPK2eQ#0KY9C-gaD+aK{>5VP81RKd{6jfI69Qtx`4grqFt@Vsy~C&^?VGe($Q;M&2*<_zqjcny|I`2&tgxnu(GRaMVfK|1cK+!K*! zJMC{*>H^kL_if#Fj*@{-s-F7^WIe2$?>%JHOY;)58qYoh2~eK`vR?EOS3mI|_rF-R1J}OY&~g1~Z_kP7X%MEM~yzjS0h~FbDso zL~iMctu756auQ?%5!CnP_5F;oQ!@J6NmzgV^ISmEh zHK@6%9)8hJ?OY^d?rh^79O&igS_orQX zeO%gObpcJT&$I5c%Z)t9xPh_2^Q5;@c)2aET+CGOXp&WEr#&vY$IL~r?7TpLSA$b- z#Rk9loO+bL{W#-}cXDv7IpH4AaQ+*chE!}9p@8j%ZL_Cw-fJ9KPrDm13mz(AnSJ|E zL1ra+Z+KlCwddaEkw*~qmTBojM{bOHA7XCQjiCL)uU7Yt37MKIs_vYy3*-F(hm#me z{Zc#|RpAzerZ20Fh%p||737ysDx!ykZP_4oeL_VpKwPqbR${Q+NwMEs9O1|!L*9m> zGud{}zrM#TzrQOpKDc#!M_(kSMYdarD#VDi{pqmnwSsEi4cHBdYnWx3wqeqw{J|+C zrt^Q;cTg=D$NxQ!4R=~CWT^hqVZM*LBc9PLBMDVtBvuU_*p7M8m8J>j8qi6WU}&lu zXldmd`HpyZoj-wcS!NY9zmXwVjqNQ2@AK*tin#x(#Tj!}d+cH*xhf)uV_H(p*uFxZ zipHA53;991g4re!MVZquwo;yi3_-bDTJc{n#!TB2;8A#gh<_e%tPXh{fy<;LIhPyS z?y?eRj|u{pud4hT4`bXuCd$_HWwM!}46e?1{y*k8y?ayJSc|~cjS^fTlu_X>~eiiEAy?8H13+mIEYqTP#AS7gSyW_k? z8cdP$^^$AfWM!x`J9YN!fq-c8kCdmR{esMkAOr_YNg$;dotuTrV`wvU_{&FgU?1!`_ccA3W>Xs``AEjIeBKRga_r;H@v>3OPr_O9T0Ktvvx6FA~WXYAJo7haWS5 zwaU1h5>bI~mVf_SxbIlUf)Vw|UHb^i&sa7mbHB}zG#M5JJ!5!@4Z&&L0llTq-8LTP zs3gTFvf?6&y70WVGar7t2Wk{Sbl0Ss{;#&qG%TqsjN^z)BBoTRqeFt3k-KIo8fuRF zIOA@Lnnok!NG0YHT2ASxk+^4OZV8oEZe(d@ph&qZl`ATfU}>g4xvw+XT%7qZALjdg z&OP@&?|bgKzw^HTzv{&NfWc%Sy)vd6^5;_~g7$3b=@{Cm0nX)XxYp`;uzXwN0!pRw zvW1j)0qGKpGt43y`wH3f#5I5FG^1C}*>=(+?sJmAnzzmG*@4N|7mN6xQZS+D`jR6B zX4WDO$Wi8ggkldl^}!VILaGf##3Y9oU*nXy^pe)%~$XzJZ=^h%}<_Gm;Jo>w zGV*oY(V3~^Lw2wa(5qQkh#BP~_uf88xo+w1A~1e;ElEicV+AcoBF3UTaV9dNHVYB=x3hR^X=U`Pi-dz)@H7&QaHCN3b!l&hMul9e1#=XqSo96kT8BKB3L* zTDX8#?A;gk%Gy<)bCE7iz74=+vA@v~GX4ZDH&pOZd!b(^PFqPsqtYdv{3C}xZQf1n zCoBxB5;_}?hdZ27%#hP<-sn#2ns;u%vCY#Ru*E>dRSaZO_!r0{L2@P=rM~AozS?T= zc*9rU>QwVwFDHF%hE!HItX}dg&EQ*T`bTeAe;%Gf=ZcCqT8z}qy zL3ZW^rvCw-U!EMg`bjpL1$myJi_8Un_CFI(wZ5z~3^Y8+?Hjb^$60{8gr{E8v7S|D zlwDYU)uu&QcLhjZN7FJgOWwS%mGH1RVo9>{juWPll$^RewAtQ3i7(msoT$FdATNWG zaXb0x0yZ`f7@_&6-%mH!t!8^Nn_iSb+atbA6ZadbSU)StX=b3TL^32O_Hcz|pv@PL zJS|Ppt}SY9Q;XW#8(zOd{U(IuPt!;rFo6%ef7FnG{A~_r%Q@+%5HBsXkJWpJSA{7f zq;4gq)o zf;0@V`n-(*KLR^jsFtWq;OXjqXhfQIF1Za+A0#b;w>l~B-?tIuLQ8BWqsqzu10^VMI zIAcTQf>XYRF^SRzIoh*97KD~YMxHSncMD~tAviL{E4}TF7cEE0dzHq2ui=a0TKOi# z@PxmYfk0qsu>?rG7S%@&T6QurS&O^uXb`7_Z5q8Xz>dPr_jNbowW``W`;B~ zhHE1!aam+c`+4Ihk+_jSoi_&r0S7>wm8YME3T+>?gTC6bpo%hJ09ds=t z=9r~jrp~Cx0DrU}jEZ0aVQwsz|0^rM3y2v2SGTS2kYa|Iw?6BEw>yJxh+9A2y{*NI zw<5A)EDn<-L>`3212~2e&cmDmko$k2H()?mcq4HqZcax64`_N9rI0DL_bxErgb7hm zoL4Y0TcwLSPCB7dfylC7{1`DUo{0EHZHJ07UBF+YwviL~k@`h<_yIs20_1G(YFGWk H>BPSQ^G4nZ diff --git a/.automaker/images/1765408161734-damvl2960_Screenshot_2025-12-10_at_6.09.19_PM.png b/.automaker/images/1765408161734-damvl2960_Screenshot_2025-12-10_at_6.09.19_PM.png deleted file mode 100644 index 77e03afafe1559899cbf49c04129da23d61178a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37452 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 diff --git a/app/electron/auto-mode-service.js b/app/electron/auto-mode-service.js index fbc2f232..20c75246 100644 --- a/app/electron/auto-mode-service.js +++ b/app/electron/auto-mode-service.js @@ -20,11 +20,64 @@ class AutoModeService { constructor() { // Track multiple concurrent feature executions this.runningFeatures = new Map(); // featureId -> { abortController, query, projectPath, sendToRenderer } - this.autoLoopRunning = false; // Separate flag for the auto loop - this.autoLoopAbortController = null; - this.autoLoopInterval = null; // Timer for periodic checking + + // Per-project auto loop state (keyed by projectPath) + this.projectLoops = new Map(); // projectPath -> { isRunning, interval, abortController, sendToRenderer, maxConcurrency } + this.checkIntervalMs = 5000; // Check every 5 seconds - this.maxConcurrency = 3; // Default max concurrency + this.maxConcurrency = 3; // Default max concurrency (global default) + } + + /** + * Get or create project loop state + */ + getProjectLoopState(projectPath) { + if (!this.projectLoops.has(projectPath)) { + this.projectLoops.set(projectPath, { + isRunning: false, + interval: null, + abortController: null, + sendToRenderer: null, + maxConcurrency: this.maxConcurrency, + }); + } + return this.projectLoops.get(projectPath); + } + + /** + * Check if any project has auto mode running + */ + hasAnyAutoLoopRunning() { + for (const [, state] of this.projectLoops) { + if (state.isRunning) return true; + } + return false; + } + + /** + * Get running features for a specific project + */ + getRunningFeaturesForProject(projectPath) { + const features = []; + for (const [featureId, execution] of this.runningFeatures) { + if (execution.projectPath === projectPath) { + features.push(featureId); + } + } + return features; + } + + /** + * Count running features for a specific project + */ + getRunningCountForProject(projectPath) { + let count = 0; + for (const [, execution] of this.runningFeatures) { + if (execution.projectPath === projectPath) { + count++; + } + } + return count; } /** @@ -43,6 +96,18 @@ class AutoModeService { return context; } + /** + * Helper to emit event with projectPath included + */ + emitEvent(projectPath, sendToRenderer, event) { + if (sendToRenderer) { + sendToRenderer({ + ...event, + projectPath, + }); + } + } + /** * Setup worktree for a feature * Creates an isolated git worktree where the agent can work @@ -65,7 +130,7 @@ class AutoModeService { return { useWorktree: false, workPath: projectPath }; } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_progress", featureId: feature.id, content: "Creating isolated worktree for feature...\n", @@ -75,7 +140,7 @@ class AutoModeService { if (!result.success) { console.warn(`[AutoMode] Failed to create worktree: ${result.error}. Falling back to main project.`); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_progress", featureId: feature.id, content: `Warning: Could not create worktree (${result.error}). Working directly on main project.\n`, @@ -84,7 +149,7 @@ class AutoModeService { } console.log(`[AutoMode] Created worktree at: ${result.worktreePath}, branch: ${result.branchName}`); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_progress", featureId: feature.id, content: `Working in isolated branch: ${result.branchName}\n`, @@ -107,46 +172,56 @@ class AutoModeService { } /** - * Start auto mode - continuously implement features + * Start auto mode for a specific project - continuously implement features + * Each project can have its own independent auto mode loop */ async start({ projectPath, sendToRenderer, maxConcurrency }) { - if (this.autoLoopRunning) { - throw new Error("Auto mode loop is already running"); + const projectState = this.getProjectLoopState(projectPath); + + if (projectState.isRunning) { + throw new Error(`Auto mode loop is already running for project: ${projectPath}`); } - this.autoLoopRunning = true; - this.maxConcurrency = maxConcurrency || 3; + projectState.isRunning = true; + projectState.maxConcurrency = maxConcurrency || 3; + projectState.sendToRenderer = sendToRenderer; console.log( - `[AutoMode] Starting auto mode for project: ${projectPath} with max concurrency: ${this.maxConcurrency}` + `[AutoMode] Starting auto mode for project: ${projectPath} with max concurrency: ${projectState.maxConcurrency}` ); - // Start the periodic checking loop - this.runPeriodicLoop(projectPath, sendToRenderer); + // Start the periodic checking loop for this project + this.runPeriodicLoopForProject(projectPath); return { success: true }; } /** - * Stop auto mode - stops the auto loop but lets running features complete + * Stop auto mode for a specific project - stops the auto loop but lets running features complete * This only turns off the auto toggle to prevent picking up new features. * Running tasks will continue until they complete naturally. */ - async stop() { - console.log("[AutoMode] Stopping auto mode (letting running features complete)"); + async stop({ projectPath }) { + console.log(`[AutoMode] Stopping auto mode for project: ${projectPath} (letting running features complete)`); - this.autoLoopRunning = false; + const projectState = this.projectLoops.get(projectPath); + if (!projectState) { + console.log(`[AutoMode] No auto mode state found for project: ${projectPath}`); + return { success: true, runningFeatures: 0 }; + } - // Clear the interval timer - if (this.autoLoopInterval) { - clearInterval(this.autoLoopInterval); - this.autoLoopInterval = null; + projectState.isRunning = false; + + // Clear the interval timer for this project + if (projectState.interval) { + clearInterval(projectState.interval); + projectState.interval = null; } // Abort auto loop if running - if (this.autoLoopAbortController) { - this.autoLoopAbortController.abort(); - this.autoLoopAbortController = null; + if (projectState.abortController) { + projectState.abortController.abort(); + projectState.abortController = null; } // NOTE: We intentionally do NOT abort running features here. @@ -154,23 +229,58 @@ class AutoModeService { // from being picked up. Running features will complete naturally. // Use stopFeature() to cancel a specific running feature if needed. - const runningCount = this.runningFeatures.size; - console.log(`[AutoMode] Auto loop stopped. ${runningCount} feature(s) still running and will complete.`); + const runningCount = this.getRunningCountForProject(projectPath); + console.log(`[AutoMode] Auto loop stopped for ${projectPath}. ${runningCount} feature(s) still running and will complete.`); return { success: true, runningFeatures: runningCount }; } /** - * Get status of auto mode + * Get status of auto mode (global and per-project) */ - getStatus() { + getStatus({ projectPath } = {}) { + // If projectPath is specified, return status for that project + if (projectPath) { + const projectState = this.projectLoops.get(projectPath); + return { + autoLoopRunning: projectState?.isRunning || false, + runningFeatures: this.getRunningFeaturesForProject(projectPath), + runningCount: this.getRunningCountForProject(projectPath), + }; + } + + // Otherwise return global status + const allRunningProjects = []; + for (const [path, state] of this.projectLoops) { + if (state.isRunning) { + allRunningProjects.push(path); + } + } + return { - autoLoopRunning: this.autoLoopRunning, + autoLoopRunning: this.hasAnyAutoLoopRunning(), + runningProjects: allRunningProjects, runningFeatures: Array.from(this.runningFeatures.keys()), runningCount: this.runningFeatures.size, }; } + /** + * Get status for all projects with auto mode + */ + getAllProjectStatuses() { + const statuses = {}; + for (const [projectPath, state] of this.projectLoops) { + statuses[projectPath] = { + isRunning: state.isRunning, + runningFeatures: this.getRunningFeaturesForProject(projectPath), + runningCount: this.getRunningCountForProject(projectPath), + maxConcurrency: state.maxConcurrency, + }; + } + return statuses; + } + /** * Run a specific feature by ID * @param {string} projectPath - Path to the project @@ -218,7 +328,7 @@ class AutoModeService { projectPath ); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: { ...feature, worktreePath: worktreeSetup.workPath, branchName: worktreeSetup.branchName }, @@ -253,7 +363,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: result.passes, @@ -288,7 +398,7 @@ class AutoModeService { console.error("[AutoMode] Failed to update feature status after error:", statusError); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -333,7 +443,7 @@ class AutoModeService { console.log(`[AutoMode] Verifying feature: ${feature.description}`); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: feature, @@ -357,7 +467,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: result.passes, @@ -392,7 +502,7 @@ class AutoModeService { console.error("[AutoMode] Failed to update feature status after error:", statusError); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -437,7 +547,7 @@ class AutoModeService { console.log(`[AutoMode] Resuming feature: ${feature.description}`); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: feature, @@ -481,7 +591,7 @@ class AutoModeService { `\n\n🔄 Auto-retry #${attempts} - Continuing implementation...\n\n` ); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_progress", featureId: feature.id, content: `\n🔄 Auto-retry #${attempts} - Agent ended early, continuing...\n`, @@ -524,7 +634,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: finalResult.passes, @@ -559,7 +669,7 @@ class AutoModeService { console.error("[AutoMode] Failed to update feature status after error:", statusError); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -572,42 +682,52 @@ class AutoModeService { } /** - * New periodic loop - checks available slots and starts features up to max concurrency + * New periodic loop for a specific project - checks available slots and starts features up to max concurrency * This loop continues running even if there are no backlog items */ - runPeriodicLoop(projectPath, sendToRenderer) { + runPeriodicLoopForProject(projectPath) { + const projectState = this.getProjectLoopState(projectPath); + console.log( - `[AutoMode] Starting periodic loop with interval: ${this.checkIntervalMs}ms` + `[AutoMode] Starting periodic loop for ${projectPath} with interval: ${this.checkIntervalMs}ms` ); // Initial check immediately - this.checkAndStartFeatures(projectPath, sendToRenderer); + this.checkAndStartFeaturesForProject(projectPath); // Then check periodically - this.autoLoopInterval = setInterval(() => { - if (this.autoLoopRunning) { - this.checkAndStartFeatures(projectPath, sendToRenderer); + projectState.interval = setInterval(() => { + if (projectState.isRunning) { + this.checkAndStartFeaturesForProject(projectPath); } }, this.checkIntervalMs); } /** - * Check how many features are running and start new ones if under max concurrency + * Check how many features are running for a specific project and start new ones if under max concurrency */ - async checkAndStartFeatures(projectPath, sendToRenderer) { + async checkAndStartFeaturesForProject(projectPath) { + const projectState = this.projectLoops.get(projectPath); + if (!projectState || !projectState.isRunning) { + return; + } + + const sendToRenderer = projectState.sendToRenderer; + const maxConcurrency = projectState.maxConcurrency; + try { - // Check how many are currently running - const currentRunningCount = this.runningFeatures.size; + // Check how many are currently running FOR THIS PROJECT + const currentRunningCount = this.getRunningCountForProject(projectPath); console.log( - `[AutoMode] Checking features - Running: ${currentRunningCount}/${this.maxConcurrency}` + `[AutoMode] [${projectPath}] Checking features - Running: ${currentRunningCount}/${maxConcurrency}` ); - // Calculate available slots - const availableSlots = this.maxConcurrency - currentRunningCount; + // Calculate available slots for this project + const availableSlots = maxConcurrency - currentRunningCount; if (availableSlots <= 0) { - console.log("[AutoMode] At max concurrency, waiting..."); + console.log(`[AutoMode] [${projectPath}] At max concurrency, waiting...`); return; } @@ -616,7 +736,7 @@ class AutoModeService { const backlogFeatures = features.filter((f) => f.status === "backlog"); if (backlogFeatures.length === 0) { - console.log("[AutoMode] No backlog features available, waiting..."); + console.log(`[AutoMode] [${projectPath}] No backlog features available, waiting...`); return; } @@ -624,7 +744,7 @@ class AutoModeService { const featuresToStart = backlogFeatures.slice(0, availableSlots); console.log( - `[AutoMode] Starting ${featuresToStart.length} feature(s) from backlog` + `[AutoMode] [${projectPath}] Starting ${featuresToStart.length} feature(s) from backlog` ); // Start each feature (don't await - run in parallel like drag operations) @@ -632,7 +752,7 @@ class AutoModeService { this.startFeatureAsync(feature, projectPath, sendToRenderer); } } catch (error) { - console.error("[AutoMode] Error checking/starting features:", error); + console.error(`[AutoMode] [${projectPath}] Error checking/starting features:`, error); } } @@ -678,7 +798,7 @@ class AutoModeService { projectPath ); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: { ...feature, worktreePath: worktreeSetup.workPath, branchName: worktreeSetup.branchName }, @@ -713,7 +833,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: result.passes, @@ -746,7 +866,7 @@ class AutoModeService { console.error("[AutoMode] Failed to update feature status after error:", statusError); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -778,7 +898,7 @@ class AutoModeService { this.runningFeatures.set(analysisId, execution); try { - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: analysisId, feature: { @@ -796,7 +916,7 @@ class AutoModeService { execution ); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: analysisId, passes: result.success, @@ -806,7 +926,7 @@ class AutoModeService { return { success: true, message: result.message }; } catch (error) { console.error("[AutoMode] Error analyzing project:", error); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: analysisId, @@ -911,7 +1031,7 @@ class AutoModeService { projectPath ); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: feature, @@ -956,7 +1076,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: result.passes, @@ -989,7 +1109,7 @@ class AutoModeService { console.error("[AutoMode] Failed to update feature status after error:", statusError); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -1021,13 +1141,13 @@ class AutoModeService { throw new Error(`Feature ${featureId} not found`); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: { ...feature, description: "Committing changes..." }, }); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_phase", featureId, phase: "action", @@ -1051,7 +1171,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: true, @@ -1061,7 +1181,7 @@ class AutoModeService { return { success: true }; } catch (error) { console.error("[AutoMode] Error committing feature:", error); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -1108,26 +1228,22 @@ class AutoModeService { // Delete context file await contextManager.deleteContextFile(projectPath, featureId); - if (sendToRenderer) { - sendToRenderer({ - type: "auto_mode_feature_complete", - featureId: featureId, - passes: false, - message: "Feature reverted - all changes discarded", - }); - } + this.emitEvent(projectPath, sendToRenderer, { + type: "auto_mode_feature_complete", + featureId: featureId, + passes: false, + message: "Feature reverted - all changes discarded", + }); console.log(`[AutoMode] Feature ${featureId} reverted successfully`); return { success: true, removedPath: result.removedPath }; } catch (error) { console.error("[AutoMode] Error reverting feature:", error); - if (sendToRenderer) { - sendToRenderer({ - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - } + this.emitEvent(projectPath, sendToRenderer, { + type: "auto_mode_error", + error: error.message, + featureId: featureId, + }); return { success: false, error: error.message }; } } @@ -1147,13 +1263,11 @@ class AutoModeService { throw new Error(`Feature ${featureId} not found`); } - if (sendToRenderer) { - sendToRenderer({ - type: "auto_mode_progress", - featureId: featureId, - content: "Merging feature branch into main...\n", - }); - } + this.emitEvent(projectPath, sendToRenderer, { + type: "auto_mode_progress", + featureId: featureId, + content: "Merging feature branch into main...\n", + }); // Merge the worktree const result = await worktreeManager.mergeWorktree(projectPath, featureId, { @@ -1171,26 +1285,22 @@ class AutoModeService { // Update feature status to verified await featureLoader.updateFeatureStatus(featureId, "verified", projectPath); - if (sendToRenderer) { - sendToRenderer({ - type: "auto_mode_feature_complete", - featureId: featureId, - passes: true, - message: `Feature merged into ${result.intoBranch}`, - }); - } + this.emitEvent(projectPath, sendToRenderer, { + type: "auto_mode_feature_complete", + featureId: featureId, + passes: true, + message: `Feature merged into ${result.intoBranch}`, + }); console.log(`[AutoMode] Feature ${featureId} merged successfully`); return { success: true, mergedBranch: result.mergedBranch }; } catch (error) { console.error("[AutoMode] Error merging feature:", error); - if (sendToRenderer) { - sendToRenderer({ - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - } + this.emitEvent(projectPath, sendToRenderer, { + type: "auto_mode_error", + error: error.message, + featureId: featureId, + }); return { success: false, error: error.message }; } } diff --git a/app/electron/main.js b/app/electron/main.js index 7f5c9c63..270f5386 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -355,6 +355,17 @@ ipcMain.handle("ping", () => { return "pong"; }); +// Open external link in default browser +ipcMain.handle("shell:openExternal", async (_, url) => { + try { + await shell.openExternal(url); + return { success: true }; + } catch (error) { + console.error("[IPC] shell:openExternal error:", error); + return { success: false, error: error.message }; + } +}); + // ============================================================================ // Agent IPC Handlers // ============================================================================ @@ -574,11 +585,11 @@ ipcMain.handle( ); /** - * Stop auto mode + * Stop auto mode for a specific project */ -ipcMain.handle("auto-mode:stop", async () => { +ipcMain.handle("auto-mode:stop", async (_, { projectPath }) => { try { - return await autoModeService.stop(); + return await autoModeService.stop({ projectPath }); } catch (error) { console.error("[IPC] auto-mode:stop error:", error); return { success: false, error: error.message }; @@ -586,11 +597,11 @@ ipcMain.handle("auto-mode:stop", async () => { }); /** - * Get auto mode status + * Get auto mode status (optionally for a specific project) */ -ipcMain.handle("auto-mode:status", () => { +ipcMain.handle("auto-mode:status", (_, { projectPath } = {}) => { try { - return { success: true, ...autoModeService.getStatus() }; + return { success: true, ...autoModeService.getStatus({ projectPath }) }; } catch (error) { console.error("[IPC] auto-mode:status error:", error); return { success: false, error: error.message }; @@ -942,9 +953,11 @@ let suggestionsExecution = null; /** * Generate feature suggestions by analyzing the project + * @param {string} projectPath - The path to the project + * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance" */ -ipcMain.handle("suggestions:generate", async (_, { projectPath }) => { - console.log("[IPC] suggestions:generate called with:", { projectPath }); +ipcMain.handle("suggestions:generate", async (_, { projectPath, suggestionType = "features" }) => { + console.log("[IPC] suggestions:generate called with:", { projectPath, suggestionType }); try { // Check if already running @@ -970,7 +983,7 @@ ipcMain.handle("suggestions:generate", async (_, { projectPath }) => { // Start generating suggestions (runs in background) featureSuggestionsService - .generateSuggestions(projectPath, sendToRenderer, suggestionsExecution) + .generateSuggestions(projectPath, sendToRenderer, suggestionsExecution, suggestionType) .catch((error) => { console.error("[IPC] suggestions:generate background error:", error); sendToRenderer({ @@ -1776,3 +1789,41 @@ ipcMain.handle( } } ); + +// ============================================================================ +// Running Agents IPC Handlers +// ============================================================================ + +/** + * Get all currently running agents across all projects + */ +ipcMain.handle("running-agents:getAll", () => { + try { + const status = autoModeService.getStatus(); + const allStatuses = autoModeService.getAllProjectStatuses(); + + // Build a list of running agents with their details + const runningAgents = []; + + for (const [projectPath, projectStatus] of Object.entries(allStatuses)) { + for (const featureId of projectStatus.runningFeatures) { + runningAgents.push({ + featureId, + projectPath, + projectName: projectPath.split(/[/\\]/).pop() || projectPath, + isAutoMode: projectStatus.isRunning, + }); + } + } + + return { + success: true, + runningAgents, + totalCount: status.runningCount, + autoLoopRunning: status.autoLoopRunning, + }; + } catch (error) { + console.error("[IPC] running-agents:getAll error:", error); + return { success: false, error: error.message }; + } +}); diff --git a/app/electron/preload.js b/app/electron/preload.js index 37230315..aa3d68c1 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -6,6 +6,9 @@ contextBridge.exposeInMainWorld("electronAPI", { // IPC test ping: () => ipcRenderer.invoke("ping"), + // Shell APIs + openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url), + // Dialog APIs openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"), openFile: (options) => ipcRenderer.invoke("dialog:openFile", options), @@ -97,15 +100,15 @@ contextBridge.exposeInMainWorld("electronAPI", { // Auto Mode API autoMode: { - // Start auto mode + // Start auto mode for a specific project start: (projectPath, maxConcurrency) => ipcRenderer.invoke("auto-mode:start", { projectPath, maxConcurrency }), - // Stop auto mode - stop: () => ipcRenderer.invoke("auto-mode:stop"), + // Stop auto mode for a specific project + stop: (projectPath) => ipcRenderer.invoke("auto-mode:stop", { projectPath }), - // Get auto mode status - status: () => ipcRenderer.invoke("auto-mode:status"), + // Get auto mode status (optionally for a specific project) + status: (projectPath) => ipcRenderer.invoke("auto-mode:status", { projectPath }), // Run a specific feature runFeature: (projectPath, featureId, useWorktrees) => @@ -243,8 +246,9 @@ contextBridge.exposeInMainWorld("electronAPI", { // Feature Suggestions API suggestions: { // Generate feature suggestions - generate: (projectPath) => - ipcRenderer.invoke("suggestions:generate", { projectPath }), + // suggestionType can be: "features", "refactoring", "security", "performance" + generate: (projectPath, suggestionType = "features") => + ipcRenderer.invoke("suggestions:generate", { projectPath, suggestionType }), // Stop generating suggestions stop: () => ipcRenderer.invoke("suggestions:stop"), @@ -382,6 +386,12 @@ contextBridge.exposeInMainWorld("electronAPI", { getAgentOutput: (projectPath, featureId) => ipcRenderer.invoke("features:getAgentOutput", { projectPath, featureId }), }, + + // Running Agents API + runningAgents: { + // Get all running agents across all projects + getAll: () => ipcRenderer.invoke("running-agents:getAll"), + }, }); // Also expose a flag to detect if we're in Electron diff --git a/app/electron/services/claude-cli-detector.js b/app/electron/services/claude-cli-detector.js index f8f4739e..7f112001 100644 --- a/app/electron/services/claude-cli-detector.js +++ b/app/electron/services/claude-cli-detector.js @@ -303,6 +303,30 @@ class ClaudeCliDetector { }; } + /** + * Get installation info and recommendations + * @returns {Object} Installation status and recommendations + */ + static getInstallationInfo() { + const detection = this.detectClaudeInstallation(); + + if (detection.installed) { + return { + status: 'installed', + method: detection.method, + version: detection.version, + path: detection.path, + recommendation: 'Claude Code CLI is ready for ultrathink' + }; + } + + return { + status: 'not_installed', + recommendation: 'Install Claude Code CLI for optimal ultrathink performance', + installCommands: this.getInstallCommands() + }; + } + /** * Get installation commands for different platforms * @returns {Object} Installation commands diff --git a/app/electron/services/feature-suggestions-service.js b/app/electron/services/feature-suggestions-service.js index 28063e66..2241e9b3 100644 --- a/app/electron/services/feature-suggestions-service.js +++ b/app/electron/services/feature-suggestions-service.js @@ -11,10 +11,14 @@ class FeatureSuggestionsService { /** * Generate feature suggestions by analyzing the project + * @param {string} projectPath - Path to the project + * @param {Function} sendToRenderer - Function to send events to renderer + * @param {Object} execution - Execution context with abort controller + * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance" */ - async generateSuggestions(projectPath, sendToRenderer, execution) { + async generateSuggestions(projectPath, sendToRenderer, execution, suggestionType = "features") { console.log( - `[FeatureSuggestions] Generating suggestions for: ${projectPath}` + `[FeatureSuggestions] Generating ${suggestionType} suggestions for: ${projectPath}` ); try { @@ -23,7 +27,7 @@ class FeatureSuggestionsService { const options = { model: "claude-sonnet-4-20250514", - systemPrompt: this.getSystemPrompt(), + systemPrompt: this.getSystemPrompt(suggestionType), maxTurns: 50, cwd: projectPath, allowedTools: ["Read", "Glob", "Grep", "Bash"], @@ -35,7 +39,7 @@ class FeatureSuggestionsService { abortController: abortController, }; - const prompt = this.buildAnalysisPrompt(); + const prompt = this.buildAnalysisPrompt(suggestionType); sendToRenderer({ type: "suggestions_progress", @@ -163,36 +167,102 @@ class FeatureSuggestionsService { /** * Get the system prompt for feature suggestion analysis + * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance" */ - getSystemPrompt() { - return `You are an expert software architect and product manager. Your job is to analyze a codebase and suggest missing features that would improve the application. + getSystemPrompt(suggestionType = "features") { + const basePrompt = `You are an expert software architect. Your job is to analyze a codebase and provide actionable suggestions. -You should: -1. Thoroughly analyze the project structure, code, and any existing documentation -2. Identify what the application does and what features it currently has (look at the .automaker/app_spec.txt file as well if it exists) -3. Generate a comprehensive list of missing features that would be valuable to users -4. Prioritize features by impact and complexity -5. Provide clear, actionable descriptions and implementation steps +You have access to file reading and search tools. Use them to understand the codebase. When analyzing, look at: - README files and documentation - Package.json, cargo.toml, or similar config files for tech stack - Source code structure and organization -- Existing features and their implementation patterns -- Common patterns in similar applications -- User experience improvements -- Developer experience improvements -- Performance optimizations -- Security enhancements +- Existing code patterns and implementation styles`; -You have access to file reading and search tools. Use them to understand the codebase.`; + switch (suggestionType) { + case "refactoring": + return `${basePrompt} + +Your specific focus is on **refactoring suggestions**. You should: +1. Identify code smells and areas that need cleanup +2. Find duplicated code that could be consolidated +3. Spot overly complex functions or classes that should be broken down +4. Look for inconsistent naming conventions or coding patterns +5. Find opportunities to improve code organization and modularity +6. Identify violations of SOLID principles or common design patterns +7. Look for dead code or unused dependencies + +Prioritize suggestions by: +- Impact on maintainability +- Risk level (lower risk refactorings first) +- Complexity of the refactoring`; + + case "security": + return `${basePrompt} + +Your specific focus is on **security vulnerabilities and improvements**. You should: +1. Identify potential security vulnerabilities (OWASP Top 10) +2. Look for hardcoded secrets, API keys, or credentials +3. Check for proper input validation and sanitization +4. Identify SQL injection, XSS, or command injection risks +5. Review authentication and authorization patterns +6. Check for secure communication (HTTPS, encryption) +7. Look for insecure dependencies or outdated packages +8. Review error handling that might leak sensitive information +9. Check for proper session management +10. Identify insecure file handling or path traversal risks + +Prioritize by severity: +- Critical: Exploitable vulnerabilities with high impact +- High: Security issues that could lead to data exposure +- Medium: Best practice violations that weaken security +- Low: Minor improvements to security posture`; + + case "performance": + return `${basePrompt} + +Your specific focus is on **performance issues and optimizations**. You should: +1. Identify N+1 query problems or inefficient database access +2. Look for unnecessary re-renders in React/frontend code +3. Find opportunities for caching or memoization +4. Identify large bundle sizes or unoptimized imports +5. Look for blocking operations that could be async +6. Find memory leaks or inefficient memory usage +7. Identify slow algorithms or data structure choices +8. Look for missing indexes in database schemas +9. Find opportunities for lazy loading or code splitting +10. Identify unnecessary network requests or API calls + +Prioritize by: +- Impact on user experience +- Frequency of the slow path +- Ease of implementation`; + + default: // "features" + return `${basePrompt} + +Your specific focus is on **missing features and improvements**. You should: +1. Identify what the application does and what features it currently has +2. Look at the .automaker/app_spec.txt file if it exists +3. Generate a comprehensive list of missing features that would be valuable to users +4. Consider user experience improvements +5. Consider developer experience improvements +6. Look at common patterns in similar applications + +Prioritize features by: +- Impact on users +- Alignment with project goals +- Complexity of implementation`; + } } /** * Build the prompt for analyzing the project + * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance" */ - buildAnalysisPrompt() { - return `Analyze this project and generate a list of suggested features that are missing or would improve the application. + buildAnalysisPrompt(suggestionType = "features") { + const commonIntro = `Analyze this project and generate a list of actionable suggestions. **Your Task:** @@ -200,13 +270,89 @@ You have access to file reading and search tools. Use them to understand the cod - Read README.md, package.json, or similar config files - Scan the source code directory structure - Identify the tech stack and frameworks used - - Look at existing features and how they're implemented + - Look at existing code and how it's implemented 2. Identify what the application does: - What is the main purpose? - - What features are already implemented? - What patterns and conventions are used? +`; + const commonOutput = ` +**CRITICAL: Output your suggestions as a JSON array** at the end of your response, formatted like this: + +\`\`\`json +[ + { + "category": "Category Name", + "description": "Clear description of the suggestion", + "steps": [ + "Step 1 to implement", + "Step 2 to implement", + "Step 3 to implement" + ], + "priority": 1, + "reasoning": "Why this is important" + } +] +\`\`\` + +**Important Guidelines:** +- Generate at least 10-15 suggestions +- Order them by priority (1 = highest priority) +- Each suggestion should have clear, actionable steps +- Be specific about what files might need to be modified +- Consider the existing tech stack and patterns + +Begin by exploring the project structure.`; + + switch (suggestionType) { + case "refactoring": + return `${commonIntro} +3. Look for refactoring opportunities: + - Find code duplication across the codebase + - Identify functions or classes that are too long or complex + - Look for inconsistent patterns or naming conventions + - Find tightly coupled code that should be decoupled + - Identify opportunities to extract reusable utilities + - Look for dead code or unused exports + - Check for proper separation of concerns + +Categories to use: "Code Smell", "Duplication", "Complexity", "Architecture", "Naming", "Dead Code", "Coupling", "Testing" +${commonOutput}`; + + case "security": + return `${commonIntro} +3. Look for security issues: + - Check for hardcoded secrets or API keys + - Look for potential injection vulnerabilities (SQL, XSS, command) + - Review authentication and authorization code + - Check input validation and sanitization + - Look for insecure dependencies + - Review error handling for information leakage + - Check for proper HTTPS/TLS usage + - Look for insecure file operations + +Categories to use: "Critical", "High", "Medium", "Low" (based on severity) +${commonOutput}`; + + case "performance": + return `${commonIntro} +3. Look for performance issues: + - Find N+1 queries or inefficient database access patterns + - Look for unnecessary re-renders in React components + - Identify missing memoization opportunities + - Check bundle size and import patterns + - Look for synchronous operations that could be async + - Find potential memory leaks + - Identify slow algorithms or data structures + - Look for missing caching opportunities + - Check for unnecessary network requests + +Categories to use: "Database", "Rendering", "Memory", "Bundle Size", "Caching", "Algorithm", "Network" +${commonOutput}`; + + default: // "features" + return `${commonIntro} 3. Generate feature suggestions: - Think about what's missing compared to similar applications - Consider user experience improvements @@ -214,45 +360,9 @@ You have access to file reading and search tools. Use them to understand the cod - Think about performance, security, and reliability - Consider testing and documentation improvements -4. **CRITICAL: Output your suggestions as a JSON array** at the end of your response, formatted like this: - -\`\`\`json -[ - { - "category": "User Experience", - "description": "Add dark mode support with system preference detection", - "steps": [ - "Create a ThemeProvider context to manage theme state", - "Add a toggle component in the settings or header", - "Implement CSS variables for theme colors", - "Add localStorage persistence for user preference" - ], - "priority": 1, - "reasoning": "Dark mode is a standard feature that improves accessibility and user comfort" - }, - { - "category": "Performance", - "description": "Implement lazy loading for heavy components", - "steps": [ - "Identify components that are heavy or rarely used", - "Use React.lazy() and Suspense for code splitting", - "Add loading states for lazy-loaded components" - ], - "priority": 2, - "reasoning": "Improves initial load time and reduces bundle size" - } -] -\`\`\` - -**Important Guidelines:** -- Generate at least 10-20 feature suggestions -- Order them by priority (1 = highest priority) -- Each feature should have clear, actionable steps -- Categories should be meaningful (e.g., "User Experience", "Performance", "Security", "Testing", "Documentation", "Developer Experience", "Accessibility", etc.) -- Be specific about what files might need to be created or modified -- Consider the existing tech stack and patterns when suggesting implementation steps - -Begin by exploring the project structure.`; +Categories to use: "User Experience", "Performance", "Security", "Testing", "Documentation", "Developer Experience", "Accessibility", etc. +${commonOutput}`; + } } /** diff --git a/app/electron/services/model-provider.js b/app/electron/services/model-provider.js index b07588c9..e6212fdf 100644 --- a/app/electron/services/model-provider.js +++ b/app/electron/services/model-provider.js @@ -251,7 +251,7 @@ class ClaudeProvider extends ModelProvider { async detectInstallation() { const claudeCliDetector = require('./claude-cli-detector'); - return claudeCliDetector.getInstallationInfo(); + return claudeCliDetector.getFullStatus(); } getAvailableModels() { diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index e320c2fd..0f50a5b0 100644 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -12,6 +12,7 @@ import { InterviewView } from "@/components/views/interview-view"; import { ContextView } from "@/components/views/context-view"; import { ProfilesView } from "@/components/views/profiles-view"; import { SetupView } from "@/components/views/setup-view"; +import { RunningAgentsView } from "@/components/views/running-agents-view"; import { useAppStore } from "@/store/app-store"; import { useSetupStore } from "@/store/setup-store"; import { getElectronAPI, isElectron } from "@/lib/electron"; @@ -178,6 +179,8 @@ export default function Home() { return ; case "profiles": return ; + case "running-agents": + return ; default: return ; } diff --git a/app/src/components/layout/sidebar.tsx b/app/src/components/layout/sidebar.tsx index 27db5679..fa4cbfdb 100644 --- a/app/src/components/layout/sidebar.tsx +++ b/app/src/components/layout/sidebar.tsx @@ -40,6 +40,8 @@ import { Radio, Monitor, Search, + Bug, + Activity, } from "lucide-react"; import { DropdownMenu, @@ -394,7 +396,8 @@ export function Sidebar() { if (!result.canceled && result.filePaths[0]) { const path = result.filePaths[0]; // Extract folder name from path (works on both Windows and Mac/Linux) - const name = path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project"; + const name = + path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project"; try { // Check if this is a brand new project (no .automaker directory) @@ -572,7 +575,10 @@ export function Sidebar() { // Handle selecting the currently highlighted project const selectHighlightedProject = useCallback(() => { - if (filteredProjects.length > 0 && selectedProjectIndex < filteredProjects.length) { + if ( + filteredProjects.length > 0 && + selectedProjectIndex < filteredProjects.length + ) { setCurrentProject(filteredProjects[selectedProjectIndex]); setIsProjectPickerOpen(false); } @@ -596,7 +602,11 @@ export function Sidebar() { } else if (event.key === "ArrowUp") { event.preventDefault(); setSelectedProjectIndex((prev) => (prev > 0 ? prev - 1 : prev)); - } else if (event.key.toLowerCase() === "p" && !event.metaKey && !event.ctrlKey) { + } else if ( + event.key.toLowerCase() === "p" && + !event.metaKey && + !event.ctrlKey + ) { // Toggle off when P is pressed (not with modifiers) while dropdown is open // Only if not typing in the search input if (document.activeElement !== projectSearchInputRef.current) { @@ -913,7 +923,10 @@ export function Sidebar() { )} - + Select theme for this project @@ -922,7 +935,10 @@ export function Sidebar() { value={currentProject.theme || ""} onValueChange={(value) => { if (currentProject) { - setProjectTheme(currentProject.id, value === "" ? null : value as any); + setProjectTheme( + currentProject.id, + value === "" ? null : (value as any) + ); } }} > @@ -932,7 +948,9 @@ export function Sidebar() { {option.label} @@ -955,21 +973,30 @@ export function Sidebar() { Project History - + Previous {shortcuts.cyclePrevProject} - + Next {shortcuts.cycleNextProject} - + Clear history @@ -1078,8 +1105,79 @@ export function Sidebar() { - {/* Bottom Section - User / Settings */} + {/* Bottom Section - Running Agents / Bug Report / Settings */}

+ {/* Running Agents Link */} +
+ +
+ {/* Bug Report Link */} +
+ +
{/* Settings Link */}
diff --git a/app/src/components/ui/dialog.tsx b/app/src/components/ui/dialog.tsx index 385fd354..e8b4828f 100644 --- a/app/src/components/ui/dialog.tsx +++ b/app/src/components/ui/dialog.tsx @@ -82,7 +82,7 @@ function DialogContent({ data-slot="dialog-close" className={cn( "ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute rounded-xs opacity-70 transition-opacity cursor-pointer hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - compact ? "top-2 right-2" : "top-4 right-4" + compact ? "top-2 right-3" : "top-3 right-5" )} > diff --git a/app/src/components/ui/hotkey-button.tsx b/app/src/components/ui/hotkey-button.tsx index 0b4f10d6..f4383a85 100644 --- a/app/src/components/ui/hotkey-button.tsx +++ b/app/src/components/ui/hotkey-button.tsx @@ -56,7 +56,10 @@ function parseHotkeyConfig(hotkey: string | HotkeyConfig): HotkeyConfig { /** * Generate the display label for the hotkey */ -function getHotkeyDisplayLabel(config: HotkeyConfig, isMac: boolean): React.ReactNode { +function getHotkeyDisplayLabel( + config: HotkeyConfig, + isMac: boolean +): React.ReactNode { if (config.label) { return config.label; } @@ -73,7 +76,10 @@ function getHotkeyDisplayLabel(config: HotkeyConfig, isMac: boolean): React.Reac if (config.shift) { parts.push( - + ); @@ -134,11 +140,7 @@ function getHotkeyDisplayLabel(config: HotkeyConfig, isMac: boolean): React.Reac ); - return ( - - {parts} - - ); + return {parts}; } /** @@ -205,7 +207,11 @@ export function HotkeyButton({ // Don't trigger when typing in inputs (unless explicitly scoped or using cmdCtrl modifier) // cmdCtrl shortcuts like Cmd+Enter should work even in inputs as they're intentional submit actions - if (!scopeRef && !config.cmdCtrl && isInputElement(document.activeElement)) { + if ( + !scopeRef && + !config.cmdCtrl && + isInputElement(document.activeElement) + ) { return; } @@ -228,7 +234,8 @@ export function HotkeyButton({ // If scoped, check that the scope element is visible if (scopeRef && scopeRef.current) { const scopeEl = scopeRef.current; - const isVisible = scopeEl.offsetParent !== null || + const isVisible = + scopeEl.offsetParent !== null || getComputedStyle(scopeEl).display !== "none"; if (!isVisible) return; } @@ -259,14 +266,15 @@ export function HotkeyButton({ }, [config, hotkeyActive, handleKeyDown]); // Render the hotkey indicator - const hotkeyIndicator = config && showHotkeyIndicator ? ( - - {getHotkeyDisplayLabel(config, isMac)} - - ) : null; + const hotkeyIndicator = + config && showHotkeyIndicator ? ( + + {getHotkeyDisplayLabel(config, isMac)} + + ) : null; return ( +
+ {(Object.entries(suggestionTypeConfig) as [SuggestionType, typeof suggestionTypeConfig[SuggestionType]][]).map( + ([type, config]) => { + const Icon = config.icon; + return ( + + ); + } + )} +
) : isGenerating ? ( // Generating state - show progress @@ -410,20 +478,34 @@ export function FeatureSuggestionsDialog({

No suggestions were generated. Try running the analysis again.

- +
+ + {currentSuggestionType && ( + + )} +
)} {hasSuggestions && (
- +
+ + {currentSuggestionType && ( + + )} +
)} {onForceStop && ( diff --git a/app/src/components/views/running-agents-view.tsx b/app/src/components/views/running-agents-view.tsx new file mode 100644 index 00000000..baad1eaf --- /dev/null +++ b/app/src/components/views/running-agents-view.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Bot, Folder, Loader2, RefreshCw, Square, Activity } from "lucide-react"; +import { getElectronAPI, RunningAgent } from "@/lib/electron"; +import { useAppStore } from "@/store/app-store"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +export function RunningAgentsView() { + const [runningAgents, setRunningAgents] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const { setCurrentProject, projects, setCurrentView } = useAppStore(); + + const fetchRunningAgents = useCallback(async () => { + try { + const api = getElectronAPI(); + if (api.runningAgents) { + const result = await api.runningAgents.getAll(); + if (result.success && result.runningAgents) { + setRunningAgents(result.runningAgents); + } + } + } catch (error) { + console.error("[RunningAgentsView] Error fetching running agents:", error); + } finally { + setLoading(false); + setRefreshing(false); + } + }, []); + + // Initial fetch + useEffect(() => { + fetchRunningAgents(); + }, [fetchRunningAgents]); + + // Auto-refresh every 2 seconds + useEffect(() => { + const interval = setInterval(() => { + fetchRunningAgents(); + }, 2000); + + return () => clearInterval(interval); + }, [fetchRunningAgents]); + + // Subscribe to auto-mode events to update in real-time + useEffect(() => { + const api = getElectronAPI(); + if (!api.autoMode) return; + + const unsubscribe = api.autoMode.onEvent((event) => { + // When a feature completes or errors, refresh the list + if ( + event.type === "auto_mode_feature_complete" || + event.type === "auto_mode_error" + ) { + fetchRunningAgents(); + } + }); + + return () => { + unsubscribe(); + }; + }, [fetchRunningAgents]); + + const handleRefresh = useCallback(() => { + setRefreshing(true); + fetchRunningAgents(); + }, [fetchRunningAgents]); + + const handleStopAgent = useCallback(async (featureId: string) => { + try { + const api = getElectronAPI(); + if (api.autoMode) { + await api.autoMode.stopFeature(featureId); + // Refresh list after stopping + fetchRunningAgents(); + } + } catch (error) { + console.error("[RunningAgentsView] Error stopping agent:", error); + } + }, [fetchRunningAgents]); + + const handleNavigateToProject = useCallback((agent: RunningAgent) => { + // Find the project by path + const project = projects.find((p) => p.path === agent.projectPath); + if (project) { + setCurrentProject(project); + setCurrentView("board"); + } + }, [projects, setCurrentProject, setCurrentView]); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Running Agents

+

+ {runningAgents.length === 0 + ? "No agents currently running" + : `${runningAgents.length} agent${runningAgents.length === 1 ? "" : "s"} running across all projects`} +

+
+
+ +
+ + {/* Content */} + {runningAgents.length === 0 ? ( +
+
+ +
+

No Running Agents

+

+ Agents will appear here when they are actively working on features. + Start an agent from the Kanban board by dragging a feature to "In Progress". +

+
+ ) : ( +
+
+ {runningAgents.map((agent) => ( +
+
+ {/* Status indicator */} +
+ + + + + +
+ + {/* Agent info */} +
+
+ + {agent.featureId} + + {agent.isAutoMode && ( + + AUTO + + )} +
+ +
+
+ + {/* Actions */} +
+ + +
+
+ ))} +
+
+ )} +
+ ); +} diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 17fbd029..6df1f9cf 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -42,6 +42,8 @@ import { RefreshCw, Info, RotateCcw, + Volume2, + VolumeX, } from "lucide-react"; import { getElectronAPI } from "@/lib/electron"; import { Checkbox } from "@/components/ui/checkbox"; @@ -62,6 +64,7 @@ const NAV_ITEMS = [ { id: "codex", label: "Codex", icon: Atom }, { id: "appearance", label: "Appearance", icon: Palette }, { id: "kanban", label: "Kanban Display", icon: LayoutGrid }, + { id: "audio", label: "Audio", icon: Volume2 }, { id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 }, { id: "defaults", label: "Feature Defaults", icon: FlaskConical }, { id: "danger", label: "Danger Zone", icon: Trash2 }, @@ -83,6 +86,8 @@ export function SettingsView() { setUseWorktrees, showProfilesOnly, setShowProfilesOnly, + muteDoneSound, + setMuteDoneSound, currentProject, moveProjectToTrash, keyboardShortcuts, @@ -2132,6 +2137,55 @@ export function SettingsView() {
+ {/* Audio Section */} +
+
+
+ +

+ Audio +

+
+

+ Configure audio and notification settings. +

+
+
+ {/* Mute Done Sound Setting */} +
+
+ + setMuteDoneSound(checked === true) + } + className="mt-0.5" + data-testid="mute-done-sound-checkbox" + /> +
+ +

+ When enabled, disables the "ding" sound that + plays when an agent completes a feature. The feature + will still move to the completed column, but without + audio notification. +

+
+
+
+
+
+ {/* Feature Defaults Section */}
({ autoModeByProject: state.autoModeByProject, @@ -26,9 +28,16 @@ export function useAutoMode() { currentProject: state.currentProject, addAutoModeActivity: state.addAutoModeActivity, maxConcurrency: state.maxConcurrency, + projects: state.projects, })) ); + // Helper to look up project ID from path + const getProjectIdFromPath = useCallback((path: string): string | undefined => { + const project = projects.find(p => p.path === path); + return project?.id; + }, [projects]); + // Get project-specific auto mode state const projectId = currentProject?.id; const projectAutoModeState = useMemo(() => { @@ -42,17 +51,32 @@ export function useAutoMode() { // Check if we can start a new task based on concurrency limit const canStartNewTask = runningAutoTasks.length < maxConcurrency; - // Handle auto mode events + // Handle auto mode events - listen globally for all projects useEffect(() => { const api = getElectronAPI(); - if (!api?.autoMode || !projectId) return; + if (!api?.autoMode) return; const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => { console.log("[AutoMode Event]", event); - // Events include projectId from backend, use it to scope updates + // Events include projectPath from backend - use it to look up project ID // Fall back to current projectId if not provided in event - const eventProjectId = event.projectId ?? projectId; + let eventProjectId: string | undefined; + if ('projectPath' in event && event.projectPath) { + eventProjectId = getProjectIdFromPath(event.projectPath); + } + if (!eventProjectId && 'projectId' in event && event.projectId) { + eventProjectId = event.projectId; + } + if (!eventProjectId) { + eventProjectId = projectId; + } + + // Skip event if we couldn't determine the project + if (!eventProjectId) { + console.warn("[AutoMode] Could not determine project for event:", event); + return; + } switch (event.type) { case "auto_mode_feature_start": @@ -153,8 +177,47 @@ export function useAutoMode() { clearRunningTasks, setAutoModeRunning, addAutoModeActivity, + getProjectIdFromPath, ]); + // Restore auto mode for all projects that were running when app was closed + // This runs once on mount to restart auto loops for persisted running states + useEffect(() => { + const api = getElectronAPI(); + if (!api?.autoMode) return; + + // Find all projects that have auto mode marked as running + const projectsToRestart: Array<{ projectId: string; projectPath: string }> = []; + for (const [projectId, state] of Object.entries(autoModeByProject)) { + if (state.isRunning) { + // Find the project path for this project ID + const project = projects.find(p => p.id === projectId); + if (project) { + projectsToRestart.push({ projectId, projectPath: project.path }); + } + } + } + + // Restart auto mode for each project + for (const { projectId, projectPath } of projectsToRestart) { + console.log(`[AutoMode] Restoring auto mode for project: ${projectPath}`); + api.autoMode.start(projectPath, maxConcurrency).then(result => { + if (!result.success) { + console.error(`[AutoMode] Failed to restore auto mode for ${projectPath}:`, result.error); + // Mark as not running if we couldn't restart + setAutoModeRunning(projectId, false); + } else { + console.log(`[AutoMode] Restored auto mode for ${projectPath}`); + } + }).catch(error => { + console.error(`[AutoMode] Error restoring auto mode for ${projectPath}:`, error); + setAutoModeRunning(projectId, false); + }); + } + // Only run once on mount - intentionally empty dependency array + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // Start auto mode const start = useCallback(async () => { if (!currentProject) { @@ -199,7 +262,7 @@ export function useAutoMode() { throw new Error("Auto mode API not available"); } - const result = await api.autoMode.stop(); + const result = await api.autoMode.stop(currentProject.path); if (result.success) { setAutoModeRunning(currentProject.id, false); diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index 83fc286c..4ea65497 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -58,6 +58,26 @@ import type { // Feature type - Import from app-store import type { Feature } from "@/store/app-store"; +// Running Agent type +export interface RunningAgent { + featureId: string; + projectPath: string; + projectName: string; + isAutoMode: boolean; +} + +export interface RunningAgentsResult { + success: boolean; + runningAgents?: RunningAgent[]; + totalCount?: number; + autoLoopRunning?: boolean; + error?: string; +} + +export interface RunningAgentsAPI { + getAll: () => Promise; +} + // Feature Suggestions types export interface FeatureSuggestion { id: string; @@ -81,9 +101,12 @@ export interface SuggestionsEvent { error?: string; } +export type SuggestionType = "features" | "refactoring" | "security" | "performance"; + export interface SuggestionsAPI { generate: ( - projectPath: string + projectPath: string, + suggestionType?: SuggestionType ) => Promise<{ success: boolean; error?: string }>; stop: () => Promise<{ success: boolean; error?: string }>; status: () => Promise<{ @@ -153,15 +176,18 @@ export interface AutoModeAPI { projectPath: string, maxConcurrency?: number ) => Promise<{ success: boolean; error?: string }>; - stop: () => Promise<{ success: boolean; error?: string }>; + stop: (projectPath: string) => Promise<{ success: boolean; error?: string; runningFeatures?: number }>; stopFeature: ( featureId: string ) => Promise<{ success: boolean; error?: string }>; - status: () => Promise<{ + status: (projectPath?: string) => Promise<{ success: boolean; isRunning?: boolean; + autoLoopRunning?: boolean; // Backend uses this name instead of isRunning currentFeatureId?: string | null; runningFeatures?: string[]; + runningProjects?: string[]; + runningCount?: number; error?: string; }>; runFeature: ( @@ -205,6 +231,7 @@ export interface SaveImageResult { export interface ElectronAPI { ping: () => Promise; + openExternalLink: (url: string) => Promise<{ success: boolean; error?: string }>; openDirectory: () => Promise; openFile: (options?: object) => Promise; readFile: (filePath: string) => Promise; @@ -276,6 +303,7 @@ export interface ElectronAPI { specRegeneration?: SpecRegenerationAPI; autoMode?: AutoModeAPI; features?: FeaturesAPI; + runningAgents?: RunningAgentsAPI; setup?: { getClaudeStatus: () => Promise<{ success: boolean; @@ -392,6 +420,12 @@ export const getElectronAPI = (): ElectronAPI => { return { ping: async () => "pong (mock)", + openExternalLink: async (url: string) => { + // In web mode, open in a new tab + window.open(url, "_blank", "noopener,noreferrer"); + return { success: true }; + }, + openDirectory: async () => { // In web mode, we'll use a prompt to simulate directory selection const path = prompt( @@ -670,6 +704,9 @@ export const getElectronAPI = (): ElectronAPI => { // Mock Features API features: createMockFeaturesAPI(), + + // Mock Running Agents API + runningAgents: createMockRunningAgentsAPI(), }; }; @@ -1010,13 +1047,14 @@ function createMockAutoModeAPI(): AutoModeAPI { return { success: true }; }, - stop: async () => { + stop: async (_projectPath: string) => { mockAutoModeRunning = false; + const runningCount = mockRunningFeatures.size; mockRunningFeatures.clear(); // Clear all timeouts mockAutoModeTimeouts.forEach((timeout) => clearTimeout(timeout)); mockAutoModeTimeouts.clear(); - return { success: true }; + return { success: true, runningFeatures: runningCount }; }, stopFeature: async (featureId: string) => { @@ -1045,12 +1083,14 @@ function createMockAutoModeAPI(): AutoModeAPI { return { success: true }; }, - status: async () => { + status: async (_projectPath?: string) => { return { success: true, isRunning: mockAutoModeRunning, + autoLoopRunning: mockAutoModeRunning, currentFeatureId: mockAutoModeRunning ? "feature-0" : null, runningFeatures: Array.from(mockRunningFeatures), + runningCount: mockRunningFeatures.size, }; }, @@ -1431,7 +1471,7 @@ let mockSuggestionsTimeout: NodeJS.Timeout | null = null; function createMockSuggestionsAPI(): SuggestionsAPI { return { - generate: async (projectPath: string) => { + generate: async (projectPath: string, suggestionType: SuggestionType = "features") => { if (mockSuggestionsRunning) { return { success: false, @@ -1440,10 +1480,10 @@ function createMockSuggestionsAPI(): SuggestionsAPI { } mockSuggestionsRunning = true; - console.log(`[Mock] Generating suggestions for: ${projectPath}`); + console.log(`[Mock] Generating ${suggestionType} suggestions for: ${projectPath}`); // Simulate async suggestion generation - simulateSuggestionsGeneration(); + simulateSuggestionsGeneration(suggestionType); return { success: true }; }, @@ -1479,11 +1519,18 @@ function emitSuggestionsEvent(event: SuggestionsEvent) { mockSuggestionsCallbacks.forEach((cb) => cb(event)); } -async function simulateSuggestionsGeneration() { +async function simulateSuggestionsGeneration(suggestionType: SuggestionType = "features") { + const typeLabels: Record = { + features: "feature suggestions", + refactoring: "refactoring opportunities", + security: "security vulnerabilities", + performance: "performance issues", + }; + // Emit progress events emitSuggestionsEvent({ type: "suggestions_progress", - content: "Starting project analysis...\n", + content: `Starting project analysis for ${typeLabels[suggestionType]}...\n`, }); await new Promise((resolve) => { @@ -1514,7 +1561,7 @@ async function simulateSuggestionsGeneration() { emitSuggestionsEvent({ type: "suggestions_progress", - content: "Identifying missing features...\n", + content: `Identifying ${typeLabels[suggestionType]}...\n`, }); await new Promise((resolve) => { @@ -1522,75 +1569,184 @@ async function simulateSuggestionsGeneration() { }); if (!mockSuggestionsRunning) return; - // Generate mock suggestions - const mockSuggestions: FeatureSuggestion[] = [ - { - id: `suggestion-${Date.now()}-0`, - category: "User Experience", - description: "Add dark mode toggle with system preference detection", - steps: [ - "Create a ThemeProvider context to manage theme state", - "Add a toggle component in the settings or header", - "Implement CSS variables for theme colors", - "Add localStorage persistence for user preference", - ], - priority: 1, - reasoning: - "Dark mode is a standard feature that improves accessibility and user comfort", - }, - { - id: `suggestion-${Date.now()}-1`, - category: "Performance", - description: "Implement lazy loading for heavy components", - steps: [ - "Identify components that are heavy or rarely used", - "Use React.lazy() and Suspense for code splitting", - "Add loading states for lazy-loaded components", - ], - priority: 2, - reasoning: "Improves initial load time and reduces bundle size", - }, - { - id: `suggestion-${Date.now()}-2`, - category: "Accessibility", - description: "Add keyboard navigation support throughout the app", - steps: [ - "Implement focus management for modals and dialogs", - "Add keyboard shortcuts for common actions", - "Ensure all interactive elements are focusable", - "Add ARIA labels and roles where needed", - ], - priority: 3, - reasoning: - "Improves accessibility for users who rely on keyboard navigation", - }, - { - id: `suggestion-${Date.now()}-3`, - category: "Testing", - description: "Add comprehensive unit test coverage", - steps: [ - "Set up Jest and React Testing Library", - "Create tests for all utility functions", - "Add component tests for critical UI elements", - "Set up CI pipeline for automated testing", - ], - priority: 4, - reasoning: "Ensures code quality and prevents regressions", - }, - { - id: `suggestion-${Date.now()}-4`, - category: "Developer Experience", - description: "Add Storybook for component documentation", - steps: [ - "Install and configure Storybook", - "Create stories for all UI components", - "Add interaction tests using play functions", - "Set up Chromatic for visual regression testing", - ], - priority: 5, - reasoning: "Improves component development workflow and documentation", - }, - ]; + // Generate mock suggestions based on type + let mockSuggestions: FeatureSuggestion[]; + + switch (suggestionType) { + case "refactoring": + mockSuggestions = [ + { + id: `suggestion-${Date.now()}-0`, + category: "Code Smell", + description: "Extract duplicate validation logic into reusable utility", + steps: [ + "Identify all files with similar validation patterns", + "Create a validation utilities module", + "Replace duplicate code with utility calls", + "Add unit tests for the new utilities", + ], + priority: 1, + reasoning: "Reduces code duplication and improves maintainability", + }, + { + id: `suggestion-${Date.now()}-1`, + category: "Complexity", + description: "Break down large handleSubmit function into smaller functions", + steps: [ + "Identify the handleSubmit function in form components", + "Extract validation logic into separate function", + "Extract API call logic into separate function", + "Extract success/error handling into separate functions", + ], + priority: 2, + reasoning: "Function is too long and handles multiple responsibilities", + }, + { + id: `suggestion-${Date.now()}-2`, + category: "Architecture", + description: "Move business logic out of React components into hooks", + steps: [ + "Identify business logic in component files", + "Create custom hooks for reusable logic", + "Update components to use the new hooks", + "Add tests for the extracted hooks", + ], + priority: 3, + reasoning: "Improves separation of concerns and testability", + }, + ]; + break; + + case "security": + mockSuggestions = [ + { + id: `suggestion-${Date.now()}-0`, + category: "High", + description: "Sanitize user input before rendering to prevent XSS", + steps: [ + "Audit all places where user input is rendered", + "Implement input sanitization using DOMPurify", + "Add Content-Security-Policy headers", + "Test with common XSS payloads", + ], + priority: 1, + reasoning: "User input is rendered without proper sanitization", + }, + { + id: `suggestion-${Date.now()}-1`, + category: "Medium", + description: "Add rate limiting to authentication endpoints", + steps: [ + "Implement rate limiting middleware", + "Configure limits for login attempts", + "Add account lockout after failed attempts", + "Log suspicious activity", + ], + priority: 2, + reasoning: "Prevents brute force attacks on authentication", + }, + { + id: `suggestion-${Date.now()}-2`, + category: "Low", + description: "Remove sensitive information from error messages", + steps: [ + "Audit error handling in API routes", + "Create generic error messages for production", + "Log detailed errors server-side only", + "Implement proper error boundaries", + ], + priority: 3, + reasoning: "Error messages may leak implementation details", + }, + ]; + break; + + case "performance": + mockSuggestions = [ + { + id: `suggestion-${Date.now()}-0`, + category: "Rendering", + description: "Add React.memo to prevent unnecessary re-renders", + steps: [ + "Profile component renders with React DevTools", + "Identify components that re-render unnecessarily", + "Wrap pure components with React.memo", + "Use useCallback for event handlers passed as props", + ], + priority: 1, + reasoning: "Components re-render even when props haven't changed", + }, + { + id: `suggestion-${Date.now()}-1`, + category: "Bundle Size", + description: "Implement code splitting for route components", + steps: [ + "Use React.lazy for route components", + "Add Suspense boundaries with loading states", + "Analyze bundle with webpack-bundle-analyzer", + "Consider dynamic imports for heavy libraries", + ], + priority: 2, + reasoning: "Initial bundle is larger than necessary", + }, + { + id: `suggestion-${Date.now()}-2`, + category: "Caching", + description: "Add memoization for expensive computations", + steps: [ + "Identify expensive calculations in render", + "Use useMemo for derived data", + "Consider using react-query for server state", + "Add caching headers for static assets", + ], + priority: 3, + reasoning: "Expensive computations run on every render", + }, + ]; + break; + + default: // "features" + mockSuggestions = [ + { + id: `suggestion-${Date.now()}-0`, + category: "User Experience", + description: "Add dark mode toggle with system preference detection", + steps: [ + "Create a ThemeProvider context to manage theme state", + "Add a toggle component in the settings or header", + "Implement CSS variables for theme colors", + "Add localStorage persistence for user preference", + ], + priority: 1, + reasoning: "Dark mode is a standard feature that improves accessibility and user comfort", + }, + { + id: `suggestion-${Date.now()}-1`, + category: "Performance", + description: "Implement lazy loading for heavy components", + steps: [ + "Identify components that are heavy or rarely used", + "Use React.lazy() and Suspense for code splitting", + "Add loading states for lazy-loaded components", + ], + priority: 2, + reasoning: "Improves initial load time and reduces bundle size", + }, + { + id: `suggestion-${Date.now()}-2`, + category: "Accessibility", + description: "Add keyboard navigation support throughout the app", + steps: [ + "Implement focus management for modals and dialogs", + "Add keyboard shortcuts for common actions", + "Ensure all interactive elements are focusable", + "Add ARIA labels and roles where needed", + ], + priority: 3, + reasoning: "Improves accessibility for users who rely on keyboard navigation", + }, + ]; + } emitSuggestionsEvent({ type: "suggestions_complete", @@ -1911,6 +2067,30 @@ function createMockFeaturesAPI(): FeaturesAPI { }; } +// Mock Running Agents API implementation +function createMockRunningAgentsAPI(): RunningAgentsAPI { + return { + getAll: async () => { + console.log("[Mock] Getting all running agents"); + // Return running agents from mock auto mode state + const runningAgents: RunningAgent[] = Array.from(mockRunningFeatures).map( + (featureId) => ({ + featureId, + projectPath: "/mock/project", + projectName: "Mock Project", + isAutoMode: mockAutoModeRunning, + }) + ); + return { + success: true, + runningAgents, + totalCount: runningAgents.length, + autoLoopRunning: mockAutoModeRunning, + }; + }, + }; +} + // Utility functions for project management export interface Project { diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index 4021d4f1..1f1ceb90 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -12,7 +12,8 @@ export type ViewMode = | "tools" | "interview" | "context" - | "profiles"; + | "profiles" + | "running-agents"; export type ThemeMode = | "light" @@ -260,6 +261,9 @@ export interface AppState { // Keyboard Shortcuts keyboardShortcuts: KeyboardShortcuts; // User-defined keyboard shortcuts + // Audio Settings + muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false) + // Project Analysis projectAnalysis: ProjectAnalysis | null; isAnalyzing: boolean; @@ -367,6 +371,9 @@ export interface AppActions { setKeyboardShortcuts: (shortcuts: Partial) => void; resetKeyboardShortcuts: () => void; + // Audio Settings actions + setMuteDoneSound: (muted: boolean) => void; + // AI Profile actions addAIProfile: (profile: Omit) => void; updateAIProfile: (id: string, updates: Partial) => void; @@ -469,6 +476,7 @@ const initialState: AppState = { useWorktrees: false, // Default to disabled (worktree feature is experimental) showProfilesOnly: false, // Default to showing all options (not profiles only) keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts + muteDoneSound: false, // Default to sound enabled (not muted) aiProfiles: DEFAULT_AI_PROFILES, projectAnalysis: null, isAnalyzing: false, @@ -997,6 +1005,9 @@ export const useAppStore = create()( set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS }); }, + // Audio Settings actions + setMuteDoneSound: (muted) => set({ muteDoneSound: muted }), + // AI Profile actions addAIProfile: (profile) => { const id = `profile-${Date.now()}-${Math.random() @@ -1071,11 +1082,13 @@ export const useAppStore = create()( chatSessions: state.chatSessions, chatHistoryOpen: state.chatHistoryOpen, maxConcurrency: state.maxConcurrency, + autoModeByProject: state.autoModeByProject, kanbanCardDetailLevel: state.kanbanCardDetailLevel, defaultSkipTests: state.defaultSkipTests, useWorktrees: state.useWorktrees, showProfilesOnly: state.showProfilesOnly, keyboardShortcuts: state.keyboardShortcuts, + muteDoneSound: state.muteDoneSound, aiProfiles: state.aiProfiles, lastSelectedSessionByProject: state.lastSelectedSessionByProject, }), diff --git a/app/src/types/electron.d.ts b/app/src/types/electron.d.ts index 073487aa..46ce7e2e 100644 --- a/app/src/types/electron.d.ts +++ b/app/src/types/electron.d.ts @@ -163,18 +163,21 @@ export type AutoModeEvent = type: "auto_mode_feature_start"; featureId: string; projectId?: string; + projectPath?: string; feature: unknown; } | { type: "auto_mode_progress"; featureId: string; projectId?: string; + projectPath?: string; content: string; } | { type: "auto_mode_tool"; featureId: string; projectId?: string; + projectPath?: string; tool: string; input: unknown; } @@ -182,6 +185,7 @@ export type AutoModeEvent = type: "auto_mode_feature_complete"; featureId: string; projectId?: string; + projectPath?: string; passes: boolean; message: string; } @@ -190,22 +194,26 @@ export type AutoModeEvent = error: string; featureId?: string; projectId?: string; + projectPath?: string; } | { type: "auto_mode_complete"; message: string; projectId?: string; + projectPath?: string; } | { type: "auto_mode_phase"; featureId: string; projectId?: string; + projectPath?: string; phase: "planning" | "action" | "verification"; message: string; } | { type: "auto_mode_ultrathink_preparation"; featureId: string; + projectPath?: string; warnings: string[]; recommendations: string[]; estimatedCost?: number; @@ -264,14 +272,15 @@ export interface SpecRegenerationAPI { } export interface AutoModeAPI { - start: (projectPath: string) => Promise<{ + start: (projectPath: string, maxConcurrency?: number) => Promise<{ success: boolean; error?: string; }>; - stop: () => Promise<{ + stop: (projectPath: string) => Promise<{ success: boolean; error?: string; + runningFeatures?: number; }>; stopFeature: (featureId: string) => Promise<{ @@ -279,11 +288,14 @@ export interface AutoModeAPI { error?: string; }>; - status: () => Promise<{ + status: (projectPath?: string) => Promise<{ success: boolean; + autoLoopRunning?: boolean; isRunning?: boolean; currentFeatureId?: string | null; runningFeatures?: string[]; + runningProjects?: string[]; + runningCount?: number; error?: string; }>;