From 835bffa5601be7e2f2c9ed2a6b66878968899c39 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Thu, 30 Jul 2026 14:39:51 +0000 Subject: [PATCH] feat(web): close a session from its tab, with a two-click guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every workspace tab gains a small × posting to the existing /sessions/delete route, so closing a session no longer means a trip to the settings page. Closing kills a live agent and the button sits a few pixels from the session name, so a single click can't do it: the first click arms the button (it becomes a red "Close?" pill and the tab tints red), the second one submits. The armed state gives way to a 4s timeout, Escape, tabbing away, arming another tab, or any click elsewhere, and a double-click is swallowed rather than treated as a confirmation. No confirm() dialog: an inline onsubmit confirm is itself JavaScript, so it guards nobody when scripting is off — the arming replaces it, and the no-JS fallback stays a plain one-click POST. The button is a sibling of the tab link rather than a child: a
inside an is invalid markup, and keeping them apart also stops the tab-select click delegation from swallowing the close click. Covered by tests/sessions.nix (server-rendered markup and route) and a new tests/e2e case for all four legs of the guard. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Sxoeoon2SQcbxnaG9tExNY --- README.md | 10 ++- docs/workspace-tab-close.png | Bin 0 -> 52271 bytes modules/agent-box.nix | 121 +++++++++++++++++++++++++++++++- modules/src/settings-daemon.py | 18 ++++- modules/src/settings.css | 30 +++++++- modules/src/settings.js | 73 +++++++++++++++++++ tests/e2e/root-sessions.spec.ts | 56 +++++++++++++++ tests/sessions.nix | 7 ++ 8 files changed, 308 insertions(+), 7 deletions(-) create mode 100644 docs/workspace-tab-close.png diff --git a/README.md b/README.md index c39f401..d7f89c2 100644 --- a/README.md +++ b/README.md @@ -245,12 +245,16 @@ agent-box-session rm review # delist + kill The site root (web setups) is the terminal itself — `https:///` is a tabbed workspace, one tab per session, behind the same login as the -terminal (add sessions from the tab bar; restart/delete on the settings -page) — and agents can spawn sibling sessions themselves (it's just a file -edit on their own account — handy for "have Codex cross-check this"). +terminal (add and close sessions from the tab bar — the tab's `×` arms on +the first click and only closes on the second; restart/delete also on the +settings page) — and agents can spawn sibling sessions themselves (it's just +a file edit on their own account — handy for "have Codex cross-check this"). ![Tabbed terminal workspace: one tab per session](docs/workspace-tabs.png) +![Closing a session from its tab: the × arms on the first click and only +closes on the second](docs/workspace-tab-close.png) + Attach locally with `tmux -L agent-box attach -t ` (see `TMUX_TMPDIR` note above). In the browser, every tab is also a deep-linkable standalone terminal at diff --git a/docs/workspace-tab-close.png b/docs/workspace-tab-close.png new file mode 100644 index 0000000000000000000000000000000000000000..e1e0d30b54616c1f285a97cad94ae3e9649136c0 GIT binary patch literal 52271 zcmeFZ^;cBw7e1^apri_dv`V*hw}3-;caM~`G>X7O3k=;cbVzpy(mg}NfHXtL(Dj}t z{Cwa4;9cuo-}#BdS~K^VbKm>gdtcYJCsajA78i>Y>%oHuxS$X3)gC;+`2FC)L!Bp& zfd8-(mgzot@azHTy@bZ+v|R+c34!%J=AnFCQ!_IwlobkXXeRWiujH7S>GAQdzg_b1 zUTW;>A++u=X=rYi&xY>rFVcbU+Slej(STDv&K=*j_%^RPw;HgG&Yov@Q$AES=>t~z z?`0E+hxX_12U)aF7#{w)61RP(^7rlh^9q=Ho%Q(7#e*^l@u&a0&qYEB|k6UJlXiZJAmG56&U3E)K;|QPF?e3iOz`B{ z)>AH0_|9hHiBhB+7?hTlO21VF7bTcyJq!$1$`l!0Sh(5W(+3uj364tTb_>oZU(Ee+ ztoUv8ro|mzOfbXoD!>jfhu@Zqj|F*GqWBXG$X@6qTWf$ojOIS&pj``QQg-^DI^u zXJ_}5L+*CVpB?awq8Ay7sV-N)D& zT3{FDf@RGcI;L2^R{`$Z$xO?hjg1TG3gH~woLa`XEhB$IC;x3lfm+R=-? zj76@l?pk;Z;FiGLxayMB&`^+?nWUso+6M*=0p^sR{{Gpa&YYIgG_$d_yk9w;kMcvu z%r36b`5PM`36MM*dRp3F)4b5Q>TqjJ$14NXXzD6Xt|Tqclx0|IZJE7{o|lh-LYyml z?j>KhAr4kPD+^17aeM62+uP)%lw_gi2G}(z30WfH%gyz5Q9S<-#tB#L$H!+*=cv3H zW;&+ghe2}2OC8d_x5uIPZ4h^BcHSnKS zuOGPzUb#EFyRkC#wY%M>be5dz)z|PE7$U%EEKDqU-GlujW*;wKQ}_t#&FkzSk;H48 zn`;)dzk0PpyCX1W;!W)B%^gftwY0Q!zEe@WoDKLoO*5Z}m$}r>-tluhMiiO*|5^&1HsYsh^nO7lZ@N|@#WD~b8~;) zQwrRnwB{`RXrtXe53!4ApRZ)v6*nj6#@z3mQ8UZAy(*NIro#uv+DkqMQgRD-ced0} zUI@$BiveRnXPc#Dg5H;lBOM)wm(an+@T0?@t?l)8dPh^vbeWrFg|%t7s~zN=T| z6!YJd#R^#1wffE$-5mPpvIf5f>o!t23%!hMhB8h4%jT zDmKM#hd-W6$wuTzj!-ZXV%0n7DX*ug+8);=i(I2tPc9c?00f(_xAP)D`fD7=H!wW0 zWq18HZz~wletmA73Mt4F_nc*}NK8oZx%t_A9CNIxt)|B5duHH18BCV}W^%T!J65iM(i*3<}_LzL2FBP{m^W)M2MRB&|Oa!Dc^Bq5<{HSPt$wt=vh zEEZoi7Z){^@$>NaK(Nfs&4xs!C#5C5I^~xXC4mxDwA|ID2TEyKZLEiHy0}!8+1p{_ zQCL-?bCEx;otx8yiNLPkIsvDcv5*UDI^6;ZIKRK>npm@N_W^;P3uRR}iX&;(Jy3C(ne0U2 z4CTkD<9we(;3!^fS1Utyyyt}YS^W2gYbFS>uo(05IeA?#3MION$w*1|7uw>BuxYi} zTdxGA>oqksRb!qv&dze1*xT-GY%Fga(a_gcSM$~X_@JmX036f7VUBm)&H#M7yI~{8 z)tAKmSLehao4c#?jCtOpr4bQ*e|=(_(U>Et-Kndi%~N!|4C4cG7x-rmHB~_YbS&Y6 zG5KxF>7wkC|E;c{5v7o&iArD6QJf7F(dx@A!1#tsB)!wd)>hwGOw62sAfK(KgHVT_ zo?dNBS5Hk28s#ibpbp&n&T-JV*?xzL+e~azm}d_vBfMy8Xj1)asZsu2&>$0m zizSC@4B6eyUL$d0B&Cpqc9zF(-@{9J5wG)fUKc#vo9ev^p?ZgRe}FE5=+VGkAf|eq zG}`jA>2VBQzExcQNY;_HQU<@5P3FxTgZ6-qpwadjkB%&|>5BY*XWX!41_7qN(d)aN z(U;qT{kGmB4IcEncOtx5!v6c*(8!1$hGHQv8?k!1iG`q1OgCH--nS4k6+BYT2w3Dw zPN$f^A#Jo915J)b_gLRwV2b?VBt_)f>R+<@yrs`Ly`Z1X-H* zDAv?f9wBE!SA_RbzE`+eOB~}dp0#4d3J9Ky?4as{iQwAWc znxL+R&f#=VtrG{w@eyH>x9isHdwS@a@y+=?VKK8B-d$(a`ok|0K;qmEA2qobRKqI~Ea% z>}jhb%VS?eu-uq5=UP`CgOnw{+}%izs=rAVw%^!}Uz!vMxeLTl7X;0oQ`PuR5(Xa1 z;zQ>XO-1V1vEcao^S*ZqZbkX{*JtgbIh-d0WRB``*L%d7L_`|?&jW+;$b?J>0wkn> z`l`s3cA&3!Id!T`_s8e{AF8>G23EP3qPLhZ3`sQayUXj1(~>_ZHz6GYwh9gohKPgw zeS}W;`AimbHf4jN3XabA7sA4$5|nuzIxR$eeX)w8$QK6f-@dz9Mr-OT;4jrb-z?P$<0*)G*|s+I&gr-W_CE#r5s@i6n;=QjXl zU-tJb)hB&Z1G|U{F^?9!Osco~KL8x%j8xH8&Hk?RLN0g4AU0Mp-XD7SoyQUqZGrnF zijpfsH9U6ei&QYIW6@Bk@$Y;1m8w6%NTTt9oNSr9^# zcB451HNX4CZ|I42BXyu;9_tnW@6w)l(Op!hy9n*O3wJB4Cf7Q)a$igwwZg?onubZ5 zk|hqql#M5kAGu&-^XdUrAICe$6m_%TdTi&fX*Odl3ZWP--d(K>4Gp`YdqtEoQQg+5e0&s}R{*7B=6Uq#cEIvJ zKs;=Evh67Wtd+P`RmuG<&^v{VxBF9#Nxqep4+YqS9wL%rZ)sm9yq>PlH|Z*#=Zs*& z-k(YzCAP52Q-54>_#!AkqYpXn;qGR7z1`p6@BYb|l7oqxTBNq_OpwRaip}sOm?xf% zaf6qeyQ->AbqNs}Qji!E!-30S?W4_ZSjfe)5f-Jkkmll2jpUOKs~lHdID*(D5)vn; zB&)~jS1f+sUuwr)7$$FAhRdbr>=C-!9dY|qw9|RU^ zXZl5OKd}F5Z7$Nhl zDtg+!J(26N)OL%h)>nQLi(X($*SNc@XRSZqq-n)wdl4U2K5%T}0G4Q{ph%w$IPS|v z1FE2=hQ{{uae7Yyfu~1KKx!M0rXrqh!)Rczq7b$NP?b`{4`N(fQ>B&p0Ei^m0njg1 ze#kqFb-gYwotT81bH^nn76@ZrrDqnbG_x_Y<_5qD?-aSm*vif6#p-IO3j3bq#r?O1 zr?5v4EqUuJ#pJ>_SPd3xy`vWv(#%6jn&aOU_ailAcv;{nN}v0q%2HR&a-_Ua$A>7_ z#M=lgio}?NI@3Kzfmu@8_a8MFEu)zFyu`~Hv?*}n35i{r>&u6~n^NzQlP3iaC@5=F zsZ=2_^=D_slPbajdOL;9~e-76y{ zauL_<3)R{K>N1k6&MIwtJ9~f~kaJe^qqMwhjE{$5Zi0v5R~0AE9h*WV+Sh&c85!8v z>NGSTYC-3CH;b8Yd0iq^R}{|4W=u1q(t{&jG2$=$$5Le}u_8c97hRa%9`{_nQwwjp zWEYz}Blp^ZIXUaXR-%{ul2g+1`h6S>nkB`&uC5-YH@iOFpX+b0Zp~dCtZ2R5OUx_+ zxunP$m7}t?>tJb|b=zj??)x)21=bzh8kUxgS5FyYxaxnbojuE047@%c7?6vMR20O- z2oY(pH8Y$0sXwPeU4Ng9;iJBdXm;^lfgoY!&OSoYB2im>Chw@d2imrfl#660oohu9 zrNuU<+XGgz##Nw-o()h-9=I!7U#c3oig)%DfQ2RGZ@BrK3TKP}4Uo<6%=Y?)2Cw96 z*l$y6hAfK|-0Qrv+z@=;o6A%>I5E-M!D3}(Oj+f~7l|(thW2~AGX0blTA8YNq=FH! ze$)iSSnD=xtVO*2OW-S9!oHN?yqv;g%{o8Jou6m3!>dQ-Mr}#)$)bL+nz2d(Toi6J z`9kl1$2~c2v85XX+1OV?(N|mX5`{RAmH%sNy9E= z$*&clK2`fX-Kx!w#Q+pr@)8z6zvM6yCs%}fmp{?qbqPCE%?3)kze1z7r*i43Bj2;X zi`-b-m277>;xBo(I5kyXC&s~7uR;k1GnSQV8)y{XG;nBZC7_ZmndNO|SY_$yzmz+8 z>LyDHg6!>*Xi*+xBolmPN+>fxg$Zc$`E0!~Vq~ndBo7vb*C{FpG5ATH8OYk12s>wLz z>9Cw(KiP#lzMdBu0hlz>JNwE~TM9Jl?wRnfti0Ij4hazZRW({6Z%lnC26v!k`XlLY zVrCOq-k&O}syYXH@}*dB1FL7`QeHq&3mqL}?JmUm0^=`WM5Yd=B(PR``0IKHBWsJ7 zuiPeN4(*0z*F}4;g%O6$q{t#hfX0GcRes?iomK+ zx(|?WB>x?z!f^LT10nePP-xy*N7B(Gg;Az}XUl7`(UG5~N3cW%g$%E6apm{Tk3)VS zKyl)v+l%UN-kQ#{<$Xb=P!^Ja$^2oM(t%%_IfsA$asv0vRV%ksrbSdOb^NR;G^_jz zjJ{%S-nG9pISKdnLppmtW7*tT=CHW<5EPS4f&?@6`?Q&?Od#vr-QQ*(o(+>>!ENpo z!G(oC0$zYbGsWCHX=GBgjV5j!XUmm*e9;I6&9hx2$}R-#ela+xCcpDcNvv14cY%+O zC)b-)a%@Vu9Zb>}+VWwgDrNHb6l&_JIq#!Dq)8%<+14R=3yC(hpS`@RyO3bmG3fFj zE4yorU`&j-^+!!(ra?IayO`hFGhBsa4tar%>YAz)e#@qt%vE~DTGir@b5c`J6&#eJ zuH5ywxbosb-scp3hH(;a5WzT9-8Q=mB*+1BF27iUf?9Q^8fuvk&N8Wkw%cCi6d5#t zd=ztEdvFdV(`G-o@s`)re5ptv=P>e^quv?EI^8uhM>o(Nm|&w=thK z&+J`@SdPz*e6NkEy1RQ+I%o*P03xi#S1?6iCj+9{M8oS|mu*fTmC7%$ePmu2Vksih z>LB7p^6HgUet`?~(%*!^i*G>Te7XxyG%7BR754gkzgtA0px)qdt+mQ0T;hJ|0TCFf{grvvoZg!>#xV_DCA;y|WVDlFd+;(zs=Pk1r^1jAf zq#z(zotBa1s^U&iGuJDwB*=g4v8QA^`fFkcJ{ay-?F;7`4#g@^mNmW~F|-;ro^b5Q z8XAh|{Q77h7l41^*1YWE7XLgW-)r1g!rW|hbc`HEG~RXTof20)$thbZk@Cc&L8vWL zd-IdUkb6(^=Sozws1HI3Yo2&DnMX8z(3=!d>q6Nh#X{p*3 zQ6zjZlFTxC&v(NHW%J?gfW8Kn{up>wer-U_&g^9tS^;l?NI8dr-P*$x`(A5M#_n}@% ziQk3QO&%j;&kc2I?o-kW5Yp1ZUdP`q&dO2;2L>3_fF8o!wr<11`u=E8PG2Zw1CN3B zMSn0)`}Gxi#H*Kb+1aTf^gia=wCjTrYg+t%;@3x?*9m`P)muAYm6ZDCyt$c~SzSGy@ke#mq8Pyom-0P%9K(;D`07>9&|({=aPZfR+S*mcsMhZA5O>Mqz1rozA{`?-gj z?H=|WkF^xBjK-%D{Sp0ecubI7*!3~40l!De|9MBm)LfPwrq|iN5O5-HT7<7EtLwQq zc}??t`~G4`S5Q(g$zlUAw640^ycw({4{Gx}<@|%Y4@O@-hTwYG+#3PStB0~OK6}N$ zK-jN~hKUIXwzV>!y|(aPQg}&8WSOCapAo=}QDdHkTDD66d4C5Ny8ZuKfz87GziR4( z|33=RPm=PjK3X}Lf-Ox^+DUZ-&&hVn9>gs$%U-Vl)fyN8W&I09+rl#2+`r$!VMhab4O=WeitJ>=^ z=rSx827|2}A3KMY|1zt5|JF2%V)^JZMB)Vf5gHmAwYfPpwK*x{i>F*LI3POwiMIy< zS2lLy{qXT4)G;p)SMKYoA6c*2`9dT?7#SoGO+7u`)Z|q4PrBa|6Td&BFDf?sbDs~m z;e`Qe%Ep;6kS|)`O3vFbx**9nXrnZShK9_xw&n!zu;b&kwO_G0Z~u44;%Gh@QEc$n zf<9mBqbt5-zk$XjCzEIv@?YYV6c;nGGLBDATT@+)j1IKGZ2Vr~akQA~gnjWF3 z2t=r|{QA*nKzL@zcm3qb3EZ;UbAnR<5(4Ui3v$hYNABQIUg4NkQZRi?u@u^EW@>3E zE$spnxzn=_?fgyMzRqwIJFw^z@ut7?^9Hg{_IT(a-`XAbF)YsARMUV^)Xg zkG#DxZT0mj(i8A#ZPjUstgS6At@6UV|NBOB6~T`oK5ZQpF4j!}AUTC+T3VbB$eX;p z${qGKHCGOQyfaXH^q}TxzSYKlMuw|TfvI3s_^%pb;!qo^#pC+do)H1;W7*QnC&+-# z4?7xRK;au6j`eSyZBsxB!+^$xhlhrOO(hnKde2g}F^`lJJ#2k=n!_{fBW zJn-2mC@3m*4Aj)rbag?=CYte68D;tP38Gu9gkj$#+t|?K9|0Q@^Fc#Puk)YX6Zg*R znjkRj(9xYAi$FQzF!j&Y6YWU87Es{VY8*K(JHpjk>ztg9lkJUp#pX zxBy;+=GD~TF1P~aLAG@5kD9d;eR=xTe;3r)rcl}5mTR6Md>RBSICIxL3j;hcW#5CK zFQa=2LZ!=!5&d(2S=xLVfANp+Mf_U*AV4jPdGA!8Mg~_-B?On@zjrhS(cYH7s7)q< zN>}~8jKx1IWch+WurK0>8x|MS;K*S=5NEcsvG_HS=Vvb&zdMzfo&EgZY_Jy|0DPMO zYyA17+=e~j7jIsGh~q=_jsN+OJ-oktHUZX&Kp4+GaaUF4qx$>*Ss)gjmS4Yqu~3V+ zO-I>B)V=vX79N|;$Bzstqv;8?VDE4*JoY8)?^(+-xQ`u7OmqOlM~nssE8hQc@`!tH z>~8b=FP{iB&f5Cj>b{N9(bccUr%%?`!E$wzmdq|MUkqn&m;1S%f9n6YKZG@W6kcR3 zHi#Fcgc1Vq;bAQX4*A92`I2I$!0PXxfhZoXQA(yAln^y*{FkfGezV7-zL*IRh=rxq zyD|)fbY)#_wa2F{M1l^DgH+_ymnvlS1}-V5fwnTA0LK?FMNdQ~w;4(YTfSswiy=Qb zIaz;3NN~)Nw7j&`B*S#~VQWrH8dwMi2j>+zV5I_H+m5r$%yxErrDgV8u*CxNf~Ro> zJKI{aUt6zGTs&N4{GSU$v@y9{Tx4Z8_4&#@9f zamk9E|1wK~6VMRL5nex>stq(OewFXdJG)eo*aW5-7^E2*#JyNe*6LauAMYA0$$Fg$ zrX-XFI;h#j#qp7!8KeHrzX`Ooj2c~!Q2n`3T3Q_L=UiOXS--QGoF<|2iT^^a4TTb= z5U2OuThgr;vm+0!}zQJ`_zJb- zXlt#^zHVXND=o_foEJ-%M!(yu!NG)aaa~;!_{XH=Y zhx)r~_0Nqk&-Trdc;QqWHF?L!F!kOjQJ>FZw2Yv|MJ`&1dj2TtU?FH_&=Z^Z{tAw- zP>uWz?;l14p0#e%12x-I3L#|Q&sHe3``^Y#JQtv?u$h?$2M2qU(2>C7QQibUwsbL}&liU=xxISxGdQGzy9-ElS5HZ#3drd>{OvOp-b7Rl_m!z0fiGH@ zsySU!Qc4u5(DF?7ZLK9l<1RSziJRN`r%%qJ4gMYyM)G_SSY^FUOa7rzUZwAj?+`*k zLFho+MrR4}y~Y=Izjy!_ZaUqi7^9u3a3Lnuic?h+6mUAm`BJ_QD2Kh#+|!-nsR{8h zP6|bKghXqOIw#ivf4RQAzP^uyRyeAtj^4CLhL z6;m6{cj#iabnROHJzgnOYv8|I;t@b4=i)2y-(RXextbgthy&4RS}T!HukYYdiCA3} za@=C#W!uJuZ;p(i6vAzw522OOm6NZ%(X7#=h^eovb)CVU&bs@TL$QTdJOo69gm&h( z0HN-EJ!KEH3~Uk-cXtn`c&DB(FIy{RmKT18iF|Et_RjS4-P@x8#1fCwxydS+T0B=x zeLX%Fp1Z4SO=TrWUOxOYxzoXjVWm!~Ff=gNTwMGCZkmXTjOdr5BJikDtIhATy?KCz zs-ZzNpC9f`FtW0q&c&C6hK4Fs102NZ6Vs2jw#uOO$}T2OIXU`}J{X9U=xLCq{SS6R zxYEX1S9+vDL?l9SE0gwVHx$%OQlJ+8mQ-gvdMdQW`<1M$zIO;LHa2Z{_gjNbyhJcw z=E2t5?(VLcgID%P`}bA?Hf{H@2hBT6e;^pO_zm8;$)`ZIX%t5p?H= zpBXZHKb!9G85kSu>!0oZ*$+}Sjy~f`CLt;62a(>D)kfC*fb-efSU?hDV-r#g^q1^w z65oySWwqM6Yvp&!UtwT4I8bswBPPpDt9tsl$$^s6VV|;w2P#(|N3Th0E43u*OD-!* z&$Q^eO3(48t)cKc_-(zTQLdRncf{gXUi*BK58m-FLK(91It)ACG41?)a@~5!1<_0_ zPWNdGvHuOXNEu>v3g26s4w&3r<$KpP^LS+5!clIBASSvHzaVl)-n#&W|HQ`~O)i6N z)G4LpmY<$7lb#B^ZLK86E49akm!G%)!R%Mch&{5(vP%Z1-D`bdAYRPxzAKuLWx#l# zf5db3^yPH95!4pR-hTDb)uvEM(UBJg^B23k*ovWE&{gGbyqY#fYj#!rjNRn5^utIl zn|dNc#M|!#;lVw|%CD1=+R)H+cRcUN{ln(&#*|uiC|yW71RgX}6i0u5-A_NPeNdHC zwF3CHa(flBo_v*jvhD>rZi?eJFwk&a``xy(LMQ)F@4-vT^G4Bfb>W7f-nV06e&n)^ zbbC?ZOXN_`WmC6f>?A=K-c-^)5KXMDHrw7}rK0J8?g^2~KnI2A>2bGj^L_RwNzaik znV=|gk%Xy69~|_Qo#~sAiKu;Z95Rlu`EJa0O28c<=!E(%{J5f&a3rH0D66<>8J#B2 zmpu?=dN|(!UBYk|=6Rt{)GLuFUVR`pU?H*Ph6&f_ zvi+U`mA5wE??2MH$sidY&B)TX)Zl=tE(BOlfc&l&vk`^U?C)_dHlIqDUpk9{eSDI% z5fdNNW_z6gn9|^w1^S5trwr?E^k9AgM$@Hz3e6R-{o;S}`vb@!`VC-QL2XUSDl5;S z)*9jT>+B??B+8jQz4N_%yFXhILU#$;Y>m!Gooh%ga8^yt+9G0JK^|=Q5c(xGW*UnM z&6+yCLT!8P-FP?445y!)tEU-bE{D1KYCN+)Cl{ZP|Gt5y3 zi39KRrI7d@@LWJAASz+vB)A`cPmPO9OZ%P}XKiO@XKA^&wqoW0pwv8sHwueF7A+Zx z3=KW9gwmcxDY>TRcr4vzWKj4SU6x7uiMZ^4br8vO12f7ffI#^N-TkwZ)lwk>*hEDJ zFANP^6ODWk-NWfIJXpywF-E$o%fHtqem1X;Jc^O8wAAzU^YbuxWz<04@ZCi5~G5K|Lb^p)Na`*#0r z2oex@`SNAWYL4T;$l@3KC%U+@vc%Z(gAu5jw6tA-k~OtNAO0>^5g2#UxoNym1xB>a z*C+L42d>oUx`4H%&fQj4&BC#;ZDiM0=aG%XMnfZN2ecgY7~Fa*BUkQeKl(}*{55S7?&*5#&eSy?nO7y}G0I!^3SPJ!vV+rxmsYmyOiLg@!T z+y27^JQEV7meRA2k_=S7Kgs?urWr(?KAO7@Rz*0oDHhX#v+=I=n`OGF>k!ESw3_F&rpVCS-ou_eFw*e|{-?(|B z1i-gq0&f#^hs?8O2)C&ky-GoY#sO4s>U2eDn2%cc7<~Tvl9nAV513t=e3r?DIC%xP z*N&)B^ffo*sxiy>RPU-j+T3E`S>;71QWiW->OsOCv%X~4?5~yW4C`p=a;QEJIS-fW zRnD1Y8wCPy?XtRHAJO)+h9?_=V=GU;?qhgrQ`JWpDYqM;3;WK@C*(c2aSD2Rw2;VP z!NOK-PIu(Qk?DJ*YWhOn zS^9$C-}eCmp<6oW-fn4tySil|&Hrvu*uKXA&@eq^`W5NT&2#V$mRMtIpdrm*x2x-G z<_Oo=TI%fFTrQK}57xyReUMeJPNn|oo%O!|sEVcdJ(X^zA7VZ`E7TVvZz=H7Fsa5u zu)n4_0M?Kjdi&X0F+Z>0nSFht|^jy>J!{eKaNDpgGU)^`QMKqG#;EwG-)50WQ=q+q_qPNraB>?adqb&59Xg z!z`E8BTL9vpkvU{pGWuD8Oe{cR-(Xe_6Pynerb^<0u!sh8xq;MqB2{!6eFy275`fy$$Y>F(yTS+qmqf-iO@}3i#F471H_zb z%E=1Y3VBK~W{zFCKiWsqRa2Ob+OV|0_Ma!}p5uslKH2JTx7Yw$hRRkAb&rfFSsL?VaCPfA&zHe=c~tM*#3^2x<=R7 z8GJWb%OQWHQ;`d&as{3pc2!Nngfb}Ai(U-u_(7Z9SPf5(;ld=3hYkH9Pg$Z`sZ0?q zb(xH-k*7lUGp>4|XP(lFCINW!1g7 zQ6ptwW=3d?Zd(liq6;y*XQ)~>;Du#Po#J4n6=n0f-}it2_RYE$;M1L)oXoXBu2o9e zbEMO(dXpk;w9LR&Bxn)aX%S^{WK?8>kt3;(%UsSR{N!zI^Pk^5dW1ah`f=SY=gVi< z=&G8h!3RgcyLzUgFdUqsAkK`SP>n}m;^DI%xCwU%peq9I@WSQAwj-O8g-SOreRZDK z@A^8`=C(EghygTXM;9c`KN+XS`RWSe!1v;4tCC>GUrn6RKeV&=*4sTu3~Rn}kNvD) zJ|R4ujCzoRPAU1WM~LbjEA-`L7B_0FQ1tm=MMU0ObT| zl&(Ds=onRP$;HLRfNe-eRX4inLs>{@V$lY{^d$|yDb*udM#lc_f|{y2tK7(6-}u+H z8E1j^j{~s#)D-S}zh8U`(h3fvl%J`i)L>IEa)<)-?tzM9i(8iQwyyqJS^*9T(YCw> zo1G0@_O=^ReZB;{r7GR!hemSpWu=7b1yxm5Ig*ni8SrxZe)$SL9X&O|=Cf$ymm5ch($z(9-cfy=>F|Bjbq zeA0LOsq1=<8vDk^21-bXOa78JMnxwn)6vqrivnzJ-irs!RTZ;bLTRCZC7aE`!Or-E zNJ|UQ!m!|gZ;?47n_PtmeSq@F-IbElC(j!5;5(%VZG^>jf}%ormi-A}6t3PB>Am9; zV?UT#zGZ#+owpKulO44K*EY2}JOB`GMs1&r z?zT3&T?&vPLai~CplJuh`hDrgpJzwpWX!e#vMMpt*O>4ZzdL|h{#CR*wZ&VCI>bLCXOGidTbOd9dUNwKFuh{oc3YR zBIC59Nmb&kI_nIMdQ~=7;h~=m+PJVVOKjT7)$Qk$(0l22MrZ-nrQqLKUoTF3EykDl zNDk|?xS#^WbfW1-V(>kaJ>~-;lS}18XnHH!JQpy&ut(AGke1puB34bfZgL)Q0F3?U z%xq}%X7OHk3yR^ZFh*3mRUhAg33Tdwf^w^?by)PyH!B#cw(D8@0YXXp;O^M}3jHx1 zZ4V=A2ch+OYVvALonU^tQaS5U7ts&lS>MFqYylSIBckgDj=gY(_k$nUpFM^4vSQN9 z$~}#Y+;PGOfuFiDC|Fo1E)=H)^e{h$Oik78cAUr?YjIiERnu|0jC=(qO0X*Y&W=KA zMVb{?k@RnGmQk}OUZ`!r%DUOeRlw~@)ONJ>UCe5M=28qij8Dd)mu>9$&u#_KIUVfp z;ZgXgsjId4`U1|m-l)tP|Iv{-x(p6)Hp3qPUkVI-2?+=+wVx}Tt*{oAnr-7n)oh4m z`Y0eRbaiJ_#O`LQz47bK5dNZ<79$Id3S?bmoIZs5^=w?!xh*%kG39G?BIFOl(+-zRuQDyB$|Gx3UM^@(Z%KYvm-q%K%kY|sHjien;H zUqN@+G+$OK{lZf~M{<5tQY9@WB}E;^2@HdD&o_b1SDR;x*n!y-fN+J2poQjbZ`76; zHlIIx;wl@NEWX3B?ZgkC(!eTt)s=5&8rVkM)I0Wj6T&EN(6UA=M-Om=(G&uQhj+f& zKh*BFZ5`VN&t6v?EqrE5{Ep?mCtt|lCifPB0XBAB-Ua8o13vy;arC`z2v#-S?eH$5 zG4PR`-VX@%B3RHr6f5dq1~iu+t&HYRMp)`RMP;<&)f!!1%zg&Hh>6ogNGT~Pol=);B}@${ zd+O`zpTrlB!7M0Rgw*_gil!952_xKO^~pFBA{0nYR;M%*u>P6zca@V)_lE6k6@RJ3 zMGFHXc+g1qXMUVMZ@Kr_%CIPq!xYnmrL?u9i2}lGCszOZ^^3ob_#h^GmC-6PGRk5O z@|T6Gn7^V&J9p*P(?UBHftqTM@6&>%?DWLMn7X5bwtm+F^!i7PcHt=>I}Ijz+R&Lq z(No(*TEA%mTyuh!7Lgo#<(eW@U!}4&ocHtDM5r?^X%FJZhDW{xLl`i5_>POKzN*eF zg#;Q0w;rkf?mH85e&FnVv`;<}FR3<>`}pr|39x!WMxm*tC8sL^cteC+TMiZ*L5bjQ zY3clp4MI}XK0MQxlbw&gU>y$1*K~_jPf&>-sx%_t%czfzt_(#T^@QMSqI7pRwtH%5 zm7#IFTnNHXX@XAQhNow|Ade)}wLaK6q*z3qrtnwO3F6>jX^B|^3ZVY-M6b3qm*+^i z1EKhsxVs`-a$+iT0wVwL=u|ypv0gpk_(R?GXEJ@EFAgGYkj8K#_3bw=u=vWa%{Q0y zU(Irjqha8iZimA9|L%+d?+%goV9%)@h@M_C8Cr5qlb#m?Fm%^vyIIPzPwloELXE2Y zLgv2rf^zcH`SlZ++>g&Uk4USuJ=_z{$fX6coCS*_-=UlZmFo z9u1^%C2LAX>Mao_hPo(ObFwnf7c^P&VPbX<#G3a;7hJ@Gg@C2ph52%8>OE}qNi469 zEF!U^8E$o))8ka{;+ec%T@S~eyU~e8cjg(^eskT)E}4++?fBWX^t03x8?YJ2lhv{?vI+XFy~f2M5&gPR^lTjv zJ|bU}8P9KzMRfeEbq%RoONLH$vbC5|+qP<)HE;H%pFRGIU(e${Wl;|bUeZ+4H zn~;zSaWY;xxda+@ErzbJ!MJD2ma&qDP6YIWvak(6?MMT>h~x4D{?6wktTI!v9YX@x z7d-f0@CUc*%F5E_6xwVV*7H)ppr@LHjDX4E_eJ095}$DYI!EY|17!v_5jzLBS8+IH zm;|!I$aj_Ek|=L+At)RZm!kY#CD3>SstF?{)O7AZ1jn{qX_+xSP0&1Qz%@1~;$Qng z$~=3CUxUZyRKK|_ARoB^lj^_(t3@yT<#|u7f3TR(r?uaUJA`l3gsQ=aOoeb;X?vB} zh})us*0M;E$j~1Cwq0a;ub; zq)y1~Q&OUg8ps;Z11hI-gdE3sh+gpBase~kRcc!+8{2!UWY6yokAqxxAp*0y^wCr# z0$wbNmlgNdG`zfmVtn=L;?`%4;{bD}-W&aCGc7ev>YSNQF?|^^e%_@l3T-BO`7%EJ z=9WH|d6ynMR#^)+2xeV`o}_jn&;>7|G-(A4CG zP1OED!otD*tZtZW-Q`*6H?pa_yPo;`SYDaN!^Ttp8@)}^tS6KG-SHIpL-KFV%;FQ{ zZ?BCVxrV6$i{^kDZEP`tEZ)SFK3}S;QW|Fwu4kSP&H?|%HfnP0w)_$zO~ z-a6cNPX!5vNRYjH<&HZ2dm?UZId^r%45qFgsi+`$d6}xOqlVNxsh18ypRO?2hx?n1 zirzA8MutXlb8uv%k_RjXnH5*G^Q8<0Sz6n?^cTV9Qp(!G|0L!?$Momj>ppmIJ*ZIci5*7X(+%jJH%gRz90- zKaD6_kko4y*1*uKY8&$R)`y_9@gyizQ)Su#VrFe=0f~=4x)cPGt6lHPH(sJ9eFld* zeMN`;eLHc5bDWV0*Z@tcs+^00JhS7H5+H~$0c^=!J|;dTY9}{HR`MTA5|0cEJ7y)z zL={UWetDa%l2wwLnMfa_p&f2Y_7^;C>ZQstjlx=c7kNSBOtj0!(aVZ{S~F13&xx9J zkZ@#1gq>Nl`yLAWFqO-L(l7v@O4qP;eQvVk9w7Nsh5RYuM1({{h^kz#n*&ys?sDb+JGr(CKo%XS?sxYFO~Om_-=Wyh>2_ezN3nj5TOxYl{(XtroGe zyG`fMug5|J{1kvwfv_j)le1(efI<-wih|Chi7oDzD~pJG223h|+34&0kSgH)5XxmI zJI_5Tb$vdTj)lb#pZKi};^u!d*J^#>6qi3UGY5;lfewU)0Y99;oXGy;K26lSyfzYd z&yu(FI&l6uSO3V+LZk1_?q&GRjnFw6{{`%_T4Be1V}ip_r1hd9=|kI7Wz^f#GB4?xvuhDF@G6HUS4kM=E?;arvXre zjaB>M7gSdpBtBVHL#Oa=G@4S-#MDySQp%{KP{F9_)7e;CD^R0lA1l|{=@<*P)&K;& z>j!g3zqR#^(#t*Tps)0auc+?3_>4#NFIqwp6aW5=hwKA0bRk&qUO2)%ZCjk$XE}iy0SRTYVyTa*8_Lqo8P99h8$-2FCP{Zghk2b^)Gt z=L)0O&VFySa1SknbHGrjE;uUP_4p#YE>#IIy*zjzuJ438d`L~by0Ac=|MK_IsR`?HC&%KxLspJW3&?t^fJ^kyxDSTilIUWT4Mt5-tjk-w6+?0 z>0QRoLpKchy>YGg-FttJ{Rix0ADbUIP#z|pdE&aS z>pVXv()2?TiWpAeF7P*mUuN?`OGdyiuGjG57~wC(ysk+a!@P)7ysoaUprD|fs;sQR zsSTF=PcBk{Tek@bnFoS{zw~9u#mDKoy1{jIwCP(IUrPy_alf-Qy#IhiMO#_H#6 z)ehrcz#8>K>coe#fwC$Qkr)-#6)NT(SBL^TJ5YR)k;RatnwgoHWMucZW$Y!pX_VaD z`*`Y+wf^oz)e=WX@aHt;&exz+dL@`rc!}8I{_rnoM<+mIQ2NfF4KBY4{ck`$u4DmY z&O$uv{<)-I+48Cc;M{*c!7GJgm*k$UXGFI$sX4cC*Rn$1kw@!U|9LD6#XrCwp&)dp zECqCB#H{D9zaSpRdKwx({`_9-`>sekw4in3>`E6Ip)>gTEDFW#r9WAX# z05tQzY!)n|5L}#>2k!WQxFE-B_RiuB*fKuxUhhAzsOArQXqG}y}3b_mSmESQmzo{Vh%U&$HGAJLR+mZ`a!T2yQe+YJxz0k4Sy zK0~!3LNp5z*XsAYxy6hvphQN-qwJplyb--ue|C5QE6X$2Q>Lz#sphV(w=uVO_V?n3 zQ-GIJ;4zJ&^Pe4vwEM$E@=3k-oE?H5{rnA3SO5)Wbp;EaGRsuj%1$F95QB;Mm6jIq z>t9{xxZoeCl2XXEmnI+oJYnAt;NAN3)8)7SBM3~jvq^aW6_8%JVk7py=$Q58?27-}DPPS?*k!I13Pn3RR@T*DX+V?8DcFi09|!(K1A~UD zoG-bIKrw4>u37J6pri5wP+q1`Ztm{D?Ev+W?S zMFW~JyZ+nC##okK^_lI`jG)h=eM#SE;Eom+s+H@O15${%gUE9Hn?vVUpfQaeM;&q^9OBpQdKBo;XFSA9aHJ z)Ay3DgC&)0Z1L9SL(uuTbz?lJtPV(09Ak@M8YyNUW3<7Q#|GL#JT-lG12jM=NGaa`T7G=v%6RD_A(P&e za5CI5Hy{@(ExnD6Y$N(*G@zoQH#H?(2f0nbTN>%TR5^XI+-u-%S@S_bPA;k^z!+%# zZzBZ(MTl0AHTFNf0HFD-w4JVUJkiiFGgCGUS8NMymI{R(Ze`4te*eDJJL7G7h5PG& ziDoVtk&!Q^syPAc?8@LBsW>t-6N~w|1$8Z-JR2uL$6x%4{+eo|)hC&93zTFF|V+o06VJZW6UQuF%o<=^GA%R{$XocU_n#B2LE)-M~cpcACW!VG<1ZL3Q7*C(5v@Oux zm9p-NyxG;8Bp{JG&*5Eg3p?z2p0+f6juWK+CienpJ2iH@s_N@snwt7vIvGeiJG+HJ zg9A^=n;$Kf?*zy=S!@89&_CR>vGt+ulYfZCwhtGTpm(F~!<1Wd8I&oWR;@k}3V(G2 z2w2lxR-^;y%jxnKdt;;d-cAX>LLJt6?xN6fTdqO|4Oc+x6Kw$sx(LgLIXQvQ{QR2q zvKLcbVPQcwT6nR*K?g+76$b~tI2;89G+=vMj!wyS{@3E-uN4jYTyzIIE0cu{dd&XX zf>Zz);ZUesuzi&X;s_!li6^PYqChPKSR*rieb4#$7=QIQ2ksmTn-*thFOlDy{r%g@ z5E>pIK}3P$hp4m!0C~7Fz~Y^rp2-FV(Tdy-3jU%*ywrcY#+fiT;!Tl39lHB3^|ZB# z?DXQ~>OG69|KyRg5iN#ZP` z<_f`G=jVrD*ncT&J#eWLfYi9UJEhq1f)PbUwsNB0)Ux0AI9~}jZ2>(u21fe4Q_Jxp zze4-#NGF|%@%V4w`lk8$`1k--##EI?t;oV#Ha{vW!{OUKT1KK_)F!AN71(VRdU{%0 zTY&~hSTQSaoC#E=MH?H)5jGB1Ss58QS!uJCUyCmj9B!6qy^;VUnor{lpf$am>&h!eaRG}r z?-R~^;oSv1#6p{b{k=V?pMZe&t?g}f8R{u+v+KMwFt5p~eCgIcVeLjDBe`V!0b|Ynwas`1*5B&h>fPcSe@+_h|D=-?Z~|FjvX| zWE@yc>|GsDQH92J%20V#Y4&}0N>fRw(X!aeRWoKMi~qD3^mO%Gdq^lV1r!S#wB2$u z_FPJrOUakL9%l9={LQkD1l+FMij*7I)1aW12PQgc>AKc1H3IJP$vlO;x?0akVPWR8 z;DEq98&0|IF6=~x%1hSTj2wOrj>Gpy;yeZxk;(g4g$UwVQ!>gDAD0rpl%cT_H2xA4 z8XD>@*Y=5%m|?00uKSFrnJg0e!T{<}uwm93#jDrkt_rGyR+6=a%!!CCyS{sn3j?<7cXW zL!~9d5d5yA_8U+7&sXP5ni)fB%m(A1yHO(K1_aI)39ZsDb>yQRV+){9S(TqtPym}) z47nfnqbkzr)&|6`p8!U4a5j>xn8aHK2rE0+0sm3=twi# z?UXyVWvSZuRa+?hqWNNAWF$5$mAJ}f#_fd&uZE#tD+2>)+^H&!6zy;>^&)Ub7a~TM z{q#)B103}gWtHaqw78KW(aq6{=(Ft5(4P4Hgi}G6tbt;Q-Ni4KXws7MvYFa_I|$~Y z>}+!r!1thEgS+-ts>jB2?=t0@^dy!&0$?3BA{QZqyVv2+QvAbDPEJl(le2}9>C^Qk zS`z%S`VUQ|n)=zzPpNxI7mIIrZUT#W=CxL8>FchbNq~>Q47=^U}Tha&Ju;T1Pw0>k^h;jC#a(TL(}VI2uR{SqMVJ}mKGN-nSJNl zGBPs2q%8?JGn7{8%XL>tt4w<*ExeX=1KbAW-^oQaa&ew<~;oJsu<*BUxz=$xw z%;WMDp_P0o5BpFz84k0u1h7U6fO6`YH5SXP0`}{ z&|7z~BB0>0C1jSN=PfwXnTsmh$*4!+Q_Duo z!bJwsbl0z3P8fQ`Ofu*k1+bk-!rqTF*C)e~cc3`ZgA06%tIIiaV@-lD=n&55PR<|4 zzJH>VZMMtk2$KI)Ied7r*|1j`{4DJJxNT@>8+oWy6(SRB&t~tHhI{#XmoM$B?eZ68 z`~@W@65A;`Xg#-l*+q?X1-`MR=s{DH6 zd>SR)&0}Khyea=`_j;d6^Jag|=23v^?c2%6fh|V;+jRati5_`hFd_4b^U(laS>~D^#ne5u8i(a_nyHM1-Xx8gqY3k?KULdRGJV1vLfArw5JI@9oVkji)nZQyl6jJv4NYH^UQN!#ut)JvI})Rd$q}LH)jNC?P%+( z0RWpzz~v&-aJ%D&97MkXg&FvajDJv4kk|w^u^M514w#PfGu&7+5mGCIL@Oy1BM?|ZqHnt zo#4qARjg4X7TH}xi%8~;dq8h}xshgrdyk%7`yg-U+JMx#)o-0>bzw^9;kLmS_*X2v z!F6r#?%J!vRg2ka#DpaS+78(6P9<@UY@OX_l$=Y&z}b z(SVMCfJvL^4Gf3DHUomQ&rJ?C|9cNf0!ize!Sw2GdhUVC4V0*^zup=EqhLA=g-S6| zVuCOfe@S_H=2(2Zwgt^}$zRu|!1NGNlx`8eEZ#2!^|7t5u6D~=d-mnTpCH-0f26nJ zk?3Zafj1hRlMYpwQ8))(z|58`)r-%c`B9DbPEP5EdZ)i+fpwsN9jjZ{DDOe5s4OlUgzhN9WnLD(`xdV zzQ(f}7vr?_<%}2KWxpo)w;juS*PR^)>hjsOXJ@=Bb#6AhtJoVgKd&d~l981Gs++EF zJn(K*$c-DvXoBbO(St5C7d{GQ%QS<{-@kwS=wX)}+N4)gk2|Q@*w~oyz+I;kO!wSG zR90BeyyQ@RbBtxYFuaJ-7#x}LRP-;AGBgzR%I;rMXU@=daekClv=RwZ!2o9vkXU^` zIgE&m1Pc)h#KhRxiOZeAdFB@oDpZ2H=zH>V^2zY+O!XPh=sY!gf0?6Q z+1Q$@>V793Nq0Z7*k0j)Jpg=6_B~&jJA+5P0Yiv%?QZx6u2}SJ9+$~6A;8~9&sk`Q z>XctfM{dEoA2ryf4%GVw#bOGv!ANctR1tm)aurSivk(iJ# zTmHEnL$nzguRDbwjkx%-ruaao(^_CGk~P1+eak^ck%-fYYv+qmcXcOQe)v+vnbjRH zn>_^_!vWjx>FHZw#lHAdh#wfTQtw`cji0bZ;dfilDN{%1T##Lp&t<~| zhLa-yz%7Q8u(af2^8HWvt|1w}->;){(#Empl<%G1`tTvosbqZZD7TqaoyYdW=+CoX zZIP;Vuyg6`5}=@-?9f>|eck-&%c3300}?K?)2EQXCjW3eKPb>Ff3vW$SzTR{;v=AH zxE8lKNpzEtM)K-_+lQjXtES6Uw~!@oQT8!KkF652pe~z)DY$DERdaAx*IR#m{9H9! zTPrkf#`cWO0)Zi?hS;VlF`-;HkULyT;|g%N%xLbnxNqNJ>(ieUNMjn#hXKLSarK1z zvUUSSadAD4UGI*Py(IaT+F)+VKw%JIP4Wu}vL0$>p4%4}TwQ&wO;x95(U_}-ASYPr zP0I3#Sd_aiCU9kCtmf`nz54(&))1K!emHB zm95*}b-AeiTUCl5|rKY}jLQe=7#!Cp2Sy3gN z-u?92&6Blov)G6HjGLt%7bzKfIqk2qTVq=6*EWL8lb|mKI!a{^7hvfiZ(L zhcyD>e}|UGV$>c&I#%tO@4&ctB1@*%$)3#KASOGg3b-=&F=}0c3$au3^^0NcS6~G=(KjUh_ zhf&E-kKI{+O;*44WL*nO_7i9ZxGcekq=4Y`SKznRZ;&}**w7yZAkws=Rldv7cqZ{3 z$kPF8(v52V^-y65EN8uV`OD5}5J$hcw7kOcZsibti_>dTSerFESQ|M?a@B4Msqb|X zUS}t*DFXigZ~7txw=sk)MO^(R*mxB6`nJ%0~17|)#JF(F8TTJkl| zs`iV!Cu80B*63-oWP^aLH0y)9s8@s2E`#&asmzN-f7+VEW$F7%wPP>ld)<5EM1iLs zO<|y0|15KH*=hTSujq(PN+hH2bxHq?fdBLYQi^}?^Ub8e999>*MKAW!Pv_E`#QnX` zhejf^6-2*#8~mihIuS&F4!hV)-}HLRWBcPZT0HC_l1yI`w6G}#(<;=X)XgvKb6#gy zDTrN4S!Jxg8=~mYVi&icP0Q_Fi|C154$2aASOt7j{pje>SlL)vyrA$@W3b1Hju_)@ z{LsteNn9fF4l_mGBr9eZA?#s!erz0SZF;-%6d5g1*|)^uKF#syt8GTHey(d!v`kfM z$$k22mgLGb*zei;siodTqYG(xK;^~$eS|>WPV0o)qlU9zs<;C#i{n2=_ug7= zQ`fK{2+CZmd`?i6l_^ZT-D9lfa>YAquP*yDT z3j}7NjKcufcGQM zE-*V(e$FPEo2jbEB7pqrZS$5fY|w6|>U<}VZ^%I00aU2me8vYtIw7#`GUyrQhF4M% zJ5wPzN+*Tul0Y*FfS3NE(JC#enuQ4PTb_q-yOlt@hGkA}qIMYxnL0a%xZe*MEiqEk zyyo^!|GI&pvIZqXJ+Ml+`1#*i3olM=EofYvAfod55qhbyxG8}2E{^YrcRnm??Cbj~ zQiYC9OMkKo_vp88Ztz;i0FEjNr56P+3ruaf3Rq|->)qsWoa!?A71nQAi&gAT76}i2 z8h0H9Q1LJEhHzlYDPDMSWkyD6^zl>+ZKZ{TSJ=wr*Zk#D=Y5#VEp;}huZ=ex@R8TO zq>+1BEw3l&I^=5B?Qq-VL#pR*3^k<>YoU*^YP*0{y$0@aZvyE&JvX>u9* zquLCPDcu#(gTgu(?;e*=YJ6&yPkeQf351Ly?nh$sTO;#=-uo7->65#FIh0CexAslS z@UA|5m-;57`ElR^I7?K$(j{&@EU^w-}d74Z1 z040jNDTl`p&5dGIG?x&pu+`vK>nka%w=wLK&;Abrm(&u!@4C8;`Z4y(_g{rB(AYix z(|%AMW_Ped8&jHK#kt6gqvVE*UmKHQ)FNW!P}mNq^Xb!thP1x|Ps9u)by%kC)35R{ zia&!e!Z!_QF^%VSa1xBP!|!7r+*TmpbjI!Q9YHrpM|)D{*`Yse->rv;Zn-2#2sPBi zS_oc*JilmlvRc4C4G0PutGCJD;i^42rXqn&*S_~03Y&SKAPKsz^Zm{K%RijDM&Iv* z$8zY8KGF3#6gg^d8*w@M60Vxn{Z+(x$D5WXYs3{_3!(RR`N6@|h|{Dd?1{4YnUIVCZ-cr(_BYE+M>|$_eIG(>s{vj)`&q+9t;+7O{n53Tpa$T!-&kYb z_;gS$Fa|@3a*y3sPurEyd4=MeuG6&Ja0Lh@fChn>8PqGeuw>iI$S zpPZDGIq^;SqQ!-pF6H+h)z;P-3yh+QEh5P^F|G37*V4|tr}DgXU~ilS zrQ#tO@F_K%vjtuX*KwMmgLXA3@BUf$5?B|qIeMkdz0A>8OY9 z6FNcHe#d9>tpJ-a$k#PqaNX|}hMuD#Z>O!XF-4ys1?QK1k34(w*?z9E z6o7~2%2lKCavH>6-ah=m$np*ulup+4*9RIr{}MYMd@8wFKlR8sc?QfP#~(=xxOG{5w`zhy4xK!WXVf!-O~;Bl5jDJbl2slDz&Tx zbF-$XT`t57#^}bFXO=mCFVL~FvVe2rRgDpE>^#|TKg2$d_390aSg`CgN$6q%-m%p* zkB~yS>`u+eL*|iQv|Q|l$MtI;YEh>4l^d9wi7zl%%a?{m_>xd}Na}mQHy*^ljc;ah zVw%PL8MY~L^8{GgZywr?aOSQN1#NtC+Sxn>p#V%?DA?SysjCCuO(T7JFfH}on?*Y> z+^wg$56+*GpO6InXn&^7BgPYLtMzya1q!ylMn5O3K{-kP`RUR&iaTqn1>eQ|QTdt~ z1iWI--1K@i&c?cUlepeHz5y;9qeViyhIEpc&E}ixFe-Ok#2(3T(16Nq+(LFVs3Fs> zd_d`Y-hAO9XM8?h_2HvzRw|PMg5M3Fu;6&kA5znnD*o)`CpRKpNuiDV8a2@JX&&(cek0zgzpdR zH5=8nxT}cM5@Rk?!Y~!&xJ%=sMT!(0{t`Nd2R&VAZns~4?GmajJf$?G^#BpmZ->JZ zNuq{)Unfe^n2>zHMztM46aQiRWsTlM}BE z!^rF>5Pe`%*n?CN(3DteJ@H`jM1vo zO72;Y!7Oe)lzmC7t; ztG#HhzHH_|^Ar=0fB8OP`1;!F#d80tP^8pGzq z$8ooTrZsy1MiIKGaMX(@j$sJ2tOo{Eq9?9Yv)35yZ>uX`AS&J-I|$d-j>-qf+p+P7 zSd9EGD8^RU`W$3b(`=h<;`(AF+o%>M=p2w&%aqL&LXBLEt7Z0?mz8Avnc|ZUqOEHh z7E4__fBu{YT~?K(>gvorNE2X!+9q<9gRZ=(?q9vOY3zS@cV!0syg}>qb&4=6@+nHS zP3TN0ia|~;(@<4EDNxdlxh`!6b`o-sYNQ}99!(e?MJ9XqgJh|oljZ2RbTKgt3kw%! zU`Wmk5KX5SG%QMjS`KcSLzU}@D@$YUfE=bCt=%05fGb(pC&=(Ng|dnJV1SeeoFsZJ_c~yZCA9gpc2gO8?uNgM+Oh;mM(cdMq#J47hpOp=?B|P-8t>TpmkEYp6TJ`LsdBxb5G0m71eFYO*2hNZgy4@pftTN zRoyI%@`(Eo+gI+EIqb9GD@>t;3;m^6b3gXJ3`-$r7P%t(IGOjq31*oyjS**^V@JS7% zcWo&fgZvI?=hXLuLkQJktW{T2iT=!+Nb&@-Q61gI)6e~Dk5r2JAP&o7H z`Rn@6p6`p6T6vY&@9$*JrN4sjuaY0kvI}M@&Rj)OWpFhj0IEzv#vvjoKfzmU7BE_w ztay1jGaJL%??UY)`G|n`kZEHNyDWR?YGJ_Qol{XC^fPJs_~UH%4+A|UpDv{(m2u{}ie^>hSaFd9s_%d`y(f*ZZ z4Z=yOgZR%5DW=E2lg?t@8=E_UY)CCBh^gyCc=@j`?(5>(?sYkmj1 z4@1psKC`l`+~*}7T9bx$+ly+dt54-4dDr?;J?FD>mPNv9URdi_PwfJd^>Bc~z+x`3 zY5FL?ti$LoG8pL_A1cQdwTY)H5d8PuO=dXyXb&5S_9lXu75oTkDlK(b@NxJx&uk6B z_0CLaoTor<3i1`}b@ZSg@Vh?HyN5xj98)}Z%|Y7^ufFaW{6!wq5%y&lECAdWD)(17 zRSjYehBb2I>L8|$`t0(&&EtLAB=kmgCxpub#iCS7DnBKc+SGWT@VH8bWqBBm<&zZj z**HVWyL;2+9#xjAxSI`EUWoB}YaT_Gayd~c+ayaguN$=ucfaQ}lXUm}PcNV!9VKN%^RCBLxVj`Rhc;qtZ5{aVI%Lidj^8+gyV8!m5Y zx6;QhOUS6WqyrFMqLMYcMElOvSX>lX$5K|;iKXr$AYN)Bp3NHXc5MRodE8cRj~h@= zR_Zom^7p4|7%q!QQRALk_NK>sz90;Kty+fl6ZohE2ua3B%DTF9nW7RV9HM@ zNilA4aKSk2w0(VnQH?t`7Uc2T+iJRzc`nV35SL%2fn|idgik(jv!=f4NVvh%v2kBn zCGs9EA8=7%%Q68?ijZFN1+a0!207K8T#dsdVKl>Pj4HuML0)X3)ha6KV`|4Jdll*4fFseRdP4lj0WO738(e=`KIRn=mn76aPBvWbV#sfq!$~A#ip>o zY=SXtPezzFfNqc5gd;4i?6jPZIWaBY7Z|KT_TEJ6utKWACZyyrSzb;p`R8{Rsnuv# z%a7|zC9?J-b%HKReT^0BFg+|*H2MZ1wPf}nz?nA`sI2{+Gb0A5dJezGNsm+EcpxUA z&wpipW^B5D`*N;&(AkTY;jy`mZ2A)oFtX+N6OD!($6G{@vXcccVN+#g zv&Q8qb0!&IG5Q@@Q_dy--s?x+?jw~MJ{G{$VY7NU1XwJu<{DFqV6`;#R?wgXW!`uH z2oJ2`48+-T*|ZCqm;7+i%uIS(@2w(_j9~&f1VPH2-zIilUi#sBv-b&B%9jj^NJKCP ztKu{nvRLZ5h{yEvbG{${Ovw#ImR{2)WQc~V)r&RiujdymWTg5P5*Jm#OZp13RbbJ; zNAeQb8LTx0z`24novME?Zivoln?pXe7NnUjO|pMNmk}l)3L*vt3GM8l-JkB1YMiFg z(Z%_twYN|Hh?t;Gt5|FnK}%}VUYw6i$cgiSyGY1*(VfvdvIO|^C+j--d@EQi=YGb1 z9ebzNkh!_lORhxW-p?JX%!pbLH#eip>yVv6$4sS!9s)O!_uwW1A9`z}grWayLPYc8 zgSb_Kq`^#z|FiF|54Uql&(CJ=S6IC;U-IL2Yff8zazHdVJ{|+b*Ve}5r2yMy{6$Ad zer%=P*lQ_^anAjX34`VaBih>~NVRP({D}?efoY$>$a1?^xRhE1NA&WVejE!O7Hvo8)K}KAF>25A&dMe)yOYw} z>qq9|Bg`zH3WKZg~9pv^12z-qm@e4{!tu`WZ5U7$Cjhuc2Gl3(Cx|6!aLhO?X^L_ zO5;!Ta3QM>M20T!g@wKZ80#ec>7AvjOhs=-dL5AA!kiXaU?K5&Wb-N=dxfbu`N%Of zL%gvFwp8{VYNPM;Ssib8$*!iP=FK(474{EB?1C!wGwc!TbJzlXsoYc_K${Hs=@!-ReMJep$CcW6i3+I8X{a>J_7 zw_dd=W~9^Eg!mt9no>SpYZ8XE_uCYG-4KrRGZJK9qhB4*+y-N1d)c;eAXHlYrZ&9w zS6KLYz2`1uqHqO-I9TcG1=>Be${y<#6|4SKaj(K@>E_|LtjNS~59n+1b0^Q!KX#I5 zb`#v8;FUA1_4PFcd0fTVuG|%=!t^J!<{8-oCC-yS1MrE%DEKtodKg1x;qnL(<17Y7 zb&_UJ+Vku6SQ2Fi4~LIBCN_Ax;-~^<(_ZMYSm?;z|$1Kbd-( zI=G!2VT4s!D#L!zJeObJk-&CXCD-GPQU$Bo@~owX6<$mCh@Z$duo7@kJ&k|B;R(}Q z@calN?Tp~SqqFfVeA|uu0fJoFVufGPHxy;PP?mJxw0wg=$f__zKgnkwsoA`;uUcB3 z_vxcJNh@${l|}Q=Ox87X|2=V)U_zV#RQF{z^6CC7&~s+)pG9J*tHvhQDs35wUUn|l zQ*soUA3n@sE{D%Wt6NV8zI`&LYY1;8p^E07(Xkq@v3R%Y?;!8sl_rOKggN3vIqgE| z@|n|BPd*h<7F~AZR~IHYE)F+VJEOg2Ov@eatW8NbPp(9wDXZajLJQaX?3?*`jSlAn z$yfEKMJk-jH%dREST=rvs9y^XMzWx>GWo2rKyErg&U--;B=-C)5^QU?Yyiv3nC2EZ z+LikxImqHq)L0lG5k_2rf+u$#%(Jf2Ed8h+Ku0R0Bkyt4$wF9QiDHjt4+f}-=?ic3 z+x{X!Jp|2}9@U|Td49HUBb}{@`UI4Ai_7B{RkgnMPLtB`I&crXjyGQF9a3i|TbouL z7t?UoAWQhnRq8%zJ#*uZB44AtvYC12oxX4GNz1$r#kwkKsKPu5;14k+_gwGnIezjZ z|HS0?Hf+>NabfDqjz}tvxo614W#MCXo9Qy%H9sW`@!W0lXD&$O5KOMwa^wA;LdYe| z;|u)?Fw?)K?({c;qkB|jq>7139R~~h7}<7bK!9~>AVI{6#$ZcLT?C@bRcH=}06goG zi4Ap6Qe_{2_ixC;miPA#iq`LL&eX{ZGK_BytWZ)wLUTK(borR=(!N+OPU4Uv3%{{; zK*akwI@&xwr%kJxw&BDte4q4%`Lu`cKXX0(lch~q0<5vIi96Hd>D0%Q`Lf=S;9y!2 zSNqhghFU;BX{bDxz~iVp4HV7p-JbpWb%_jcUVZkh&NDt`0NKS|(KBmf@WDvMs;9pa zm+}t{55w(sa;&edgCH*N({n+|gF26+Vi)LsXfl65mI?Rsce5!Mo(BR63SLKEbpz6n z@&2olbAC^|*)kGfdZu~n)}JLt<~j}Fk@%0PS2T)GV@}kRwV@qAknk-gLM}E6gp0<+ znY~{EF(S){U;2%!=m*f_W-DC+-1gg(V>@ORXY);UHGpXvRpPhz)oUw1mSRrczHPvaxouuIob*%C*_n@|Neo;92%#l$URkb8#OcP))gSLcRryrN+|S5*Xp z&Qze6fLUiwVSmwgl2mwl;^AeFv#Kgd0vMwNi(M~=ZjUoEPaoW6;=SqQ^{pwZ%NIKi zkjR+PJGoQ^+kXaTEBlf_Vy9lTgb{ZpmGOU2VblH3u&l$o;pkwMR+BYRkvC~EIkx_e z&Xr@Rmdtef<*8|&{ujW-y`4%%7OyYQ%Q0rI#D37V@xQhpu$v4LXWZ;JLu-@$kIA6j zn&fpY-By~|b+cZ*cR$?vY9b99eVQxu_=>AxifNM8`wlc)6beNEBMHG4})mvda(<_dh`pevX7vg&Vpi#pw3&S`^*en%ps zF5`gz7W|R~Z>r>_8XH+23*RkmrJ@Rh!L4V9WMiP8ziwZ=# zrsC7dL7yH^mNYPo{GL7c{LLWeu^v(>8G0uqEU2@mr{eqfzc(n2jE!GZdDA+VA_oWe ztuDZt!IX8{1K!>@2P*}v*@vU_faeqhmub ztWK9{4*qU~r5M3yV$$taRyi7udKoVWfIz<(Nlw z!jvHyvh%#$J!g>Ib*vFu48&9`~Hb+PX5g}AJ&RU_bPR4Ae9GX*Ob z^JuDVME;vt6eq(l@#RBO5)HiutV2G3LwWS|g7AIQPETtK0iH_- zb40o^RWdMoOVz@+563#OB{X$o^GQ(1@>_%HBs$VH#6)Gc_V@OQw!?LAPZDw}@odk% zZEPZDBAc$Rza!m$VHYITR68#aO{HY*u6?x?=HkLY@7h@#y&^C(vpw&xu`7)vonN9A zb+_322Gor`f8*bgmY2^eZf!2Z^L&WPm~3!i98ikGLahxV0fHlC7aP3CjbasBiiHKI z)cRB~$eX3jW%o_i3k$}wdv|v??-m}400nwaM-Ndz-rFgQ&m#f9{iA7(qbBN%6W%ao z$fhsFJexw=5>82}?Xv%mvH$$AOpS&P8|bkG8F&OZ7h2B@M9EuV%40uDCM8>V-RnOM zao=B;3b;$!p7GN)e652AHJYW4wl8``Rdy}GPS#qay96GaKI%&ZiRPH9c5<9p+~3!^ zBF}pYjvgS`1T16hqA5W9LgY(J%2QjI*qAdB)pl$y47O8Myr!+z=t7K&N^pq$a^vd> zlBqJGrZK7)^6$}l3B+xoM@Pp3`~nTQjhjf;lrc;$^^{cV+H6lZ6$rBbCTa0+RiZPi zw$eJpYTTxXiRMXxt?#sqGX%Lm6v<<7oO33THsJGoSn;AVr(M#s-vsb`fF%PN=>amP zur*nmh~d2YdJ#_(7Cjh#p5b~(YcC@kJ#Kjp4~$tdW#3)&d|>VfPN*8}78D~P8SM^`03i>B3lQ_B!NJ$iv+Y;ry+1EG*CCyj2$cWF^I@cQkZ zlM>E;fVDXY4c!}qp6;{8mX;+UpS=eTZAg*R2GJ>K!uKU_IUON4Gk-KDO)+G>Rxm$ocho)n|AAyK9-MqvPX&S>=Zly{u#U zwmis))M<4J6uSqFeT`q8e@srdmX(#V>+#E)1`96ylhpN_^EV?V+E8pZvhAaV zyoiVW=J{F8WPpp!OnhP#zZWRXPoCI`hU1H#G$JPBh}(?Qp0p_@N`4@K6W13Yqn=#R zImbju?x)llubRb^wm^2owcIVyS%g-8w}*1rCF?|d{VeWYOvNTE>qwE`fSEp_RWvYi zc$IeCkSVAOGcX@*?}vF?0f)ieja*%y{~^fqvYJAAbzi1f8l>+4o(?zI7H`e_y+n!EmHCEWMYC)Z*N2tbzwBo>i3y zg~W6CFi?pghzf9?`+&l0L5X7z>bZRZl-mFWWTopzck^(dqjA=-c>$!`i(Z`WXBT8A zALM9;7mci>C0w2H+7^{DStbefoK8esmb62FPumY{GX-q=&ybC|QA%2FhX-tE8(En^ zY=yf`q_RLZ$16p}WcI(R%Ad77xnDO=pU-{FUpsyHfnf_2acTw+Yjv>>Pe4z^YrZ6rR3oP%x(6I1c}-2~&MVtw`KZx#1t1ghJH zLBU1{cwNHDo*=m%j(@Fu3lLu3S+de7MG1rmyh>yD0K{sFQk$J&R1E2d>KPi85I@0o<|@O zbGgT_B%G6OwUSl#(~|vA3U!HZW^`P2oKLZ=|2@E3sq-aqZXNI zYWiSnOVyDi@+B}Z%B8DNh~6{`2s%3goZh||c-hm@5xy~zot@p*()yh8IT!E2?rt{R z7DO}Szcg8GBL7=RSWtHM8Ce>=syK=WkB8^oSYo|8O5H=!qSP&0FfV;ciWvmB0FDQtL7d>O8a}cyCFc1 z2W+;>$c&t-H#!80IY#lV#f@20L?HgMOLtF*dQH>wkaXZSH?G#M?l2tCjpJuDq;g3ej|IMxr?l19 z3Kth|s^j=z)Kj-Z5_yS8r?h|y*FU}4PR-i&4Xhh5<)R9Ekd!dOUm#YMuTe%uR>joj zn&o>DfDm>rC1X=nRpnkwLkT#aqe1vHqH_5P~< z#uv*t(@sx8+)S;P6PQy0TdbI{e4I$WOT$&rljERMLF_vm|CzG|muyGW`+)2pJ*%0Cf?c6kf8O zwiFu|m!PXHtDA~)#(~aA_B9Z-GuG2k`CKRZ9{EWpi`tDr5)I87KG)GNAdR=DsK}!} zrJ!JMbnesT^QHrUgin?fV*!o6K5*oM*;=Koo*q#h$ z$Y{GCKlpzwi%g0-c(QXkK11JdsCS^8dVLA7{L)^kS=+jocLuv5oBzb)-}oco732*??_0ES|5Gp%E>waD;i z)PHv5!)r1=F@6@M^pCUtv*7{^ysnpT7|XBaQ~$GB%Fru986PstM+$A-0=CgL)|=|g z|2~rrBX}mZR4k_BAify7Qyu?~8bki!UwYV;6m>vH>06^jC|Hu^l!uMVPv!lB^6OD= z`uukbp35z2DZ>?ZxF3!b<=}jXq6c_P_MiMiN(8T47`$%obib@fA)bpo&$lOgCWJeK z!G(wJl9J%KzWnNwd6e>(1v{Y(qZ`Z3ICJK3wUn(UdVJ&LW0x}x{(qr$AXDoK*n_N1 z`BvX6L@kT&C%iVD&f$%-erNL!)ebz)6YyTT(?o_C)#t-&br?OIEl;I1uhlz*?7pb9-%kO{~f+TC>kx{YsHFNS;E z10xF?a!bmSibCp(9yb({(YQgF=gwn73M2fii4H2)l7&h>pEKXz&EV+ti(?u4`D=wX zD^M!32A1M1A!v%+cUq(N-JI0$r+4Eggjv;_OT+bzJJ@m94F*<7zdREy|}-j zh1P*$QYh?N)-nApd1mV3=Ey$Un1H;U&p+O#ZoO9?OF|aV#R=fsh5TpNi`D!I*Cb*3 zf^@M*R+KMPjw#)u^ueHkx(?vb06k(riR@ZajfkzRP@>#l>lre|4q~~93Z*YiNU3CY zEKA>XsP;cCRCU&(k=Ib~{fzjO;l6lVl~_sUTLrZedgTlHyg9T#+{|A{TdZSQh+0*~ z8-TYNv*y(=RHc60h+0d2W6pY^2h4(R~-?PQ4w^aqj{6qM)Lppcr@){^r|9!kY5`^a8?@k`QrC zgOTiEZXE+SPujb(!qB({f3p?bIx}2Z$dFEGy?@oEZe|NhqC6&?+3)F#RN0{zpU(%cH9^0-rKVZplrq&!t&@42lx6A?u%D@2#`>U{0m~ue!4eilb`-GzkH| zgb)&30wmbr?hZi*3+@oy-8~5wJTSQ1;BJExAh^4`4(=9YJIVLg?$++s)>iFa?A*Z3 zboX?hKJuRTeI9LP(b1fbGsI&QW(fa-)i8_a@n`ppooVFI5~X+$Yo%HjB}I%X)4|t3 zxRESkG|jlwCAJOQ4SDCMP+kk$H>A&97;;LYi|vC zj<+ScT-sbR7-_OmT=%Fx*78E$`U!NcV^Jm*z5%>KyaN*(b_)#n3e)c zvi#s+zT%ikmd&3rlg?C=MloS)eoJ(zg@v|_*AdR;_0dX9yQf6Vmxc(HaaweAB?E(l z7Z+$w7w4KS>%0}}bj8I0kczQqco?urUZDLQT@K!+=VkX#q?a@|*MwV(xr=FPz9@x% z5z|+YG7pZ5s)(wvSBb5w&Jo=-vh`?WuyYKfqS1BW6!}?AOW^dp6T%nVi#0A)duGAm zR^+qM$m45BqO&D6Go4Nz)3oaq4bn$37UUbUo4+l1z>41Wj|L56xK|ES>GPhKYOq%3 zHO?l)cbDQUK>&3K@2Sc#zTUm}iJNKF8M3C9OBEY^5r*Cn{f}lGx409erxFq%bVkM( zQ~&`BsiA>~heswfn-dDlMPh=LSJzanpCK7Tq2K-vLvygeiHw{gq}HKSXQyka;OFgq zVC2sq)Z~XJ2UDN+0Vc{#lxAxuXA-nMWAc^ARtLt0>M(P?-9mwaYdg+)`B$Arrv#s^ zTDdEQfQrbEib`V9-Ei6hKg3B+urcMjC^y9#8XY> zB|!8M&pZ{Z;ALfHukij4*$Lr5`Gdtr0Kb!vkbr}OwPNukMkTn@NypsGD5qnZnuhM9 z+eOjmSKh_+wRuMSa{UHr-15XDs*)f}-Piz&N~g-L8yj8cfFHZHI24l?R21w7%&LNY z3x__1QiVmTeMRb(HMB)VYN}GN?5LDXW46>q zZ&HL_;ogZ0^S_pRmDPMDM2OzhoFYi(Ds?n0GsAGv43VX09S2EZ5=?@$MD2r z;i{?BWLe^wsQ{$t=a~*7&;VYa?={f0h@q2#1?|zQ>v!}p-{jrmV#+)tH~F;vI41Vz zMDv(0Ig^aGGg2g31uU&E+~Tz%$!mV=NdJg$fiIMGjJ~8a2`bWxGy1h3#~(CMu+h!Y zOm)PRN2#~&G(#UHICyu?i^jyUYn6XwnZMjCv@8x=ns-Z%qsJ19QxlC-5sgvPW?+bN zKBF%v5T#xfc*@{@D}2O@B&L*dkpGQ%Ky=a0EfCYiCbNs>WnYF}hTFoXL=FNnWRnUi0` zlSkwJEoa&7bCa!t?gn=&!x&6@S(uBRd)&AbQfm0iY4`d-&4j)O3I#!XRbvVzrQa6; zIK=sg<;ihs3SB^!WO12D;?jR-;Ro`~h~O`!)zh|7cw7L(xEASEo@~2h51Hl zXc&i|WUsg|hEU~`A~z;Q!v`Rs#-ed44YDv9S?Smm!Gc2}aUMC+L<{{}Upbd9F9F22 z;$9D)LzkFzG8Cr_P2742oK*vHlQ+hpR6khT7ewav#bfgaC&aOOaVFa_tULcKC8{~m z;GM}&@}*T`+3pmm=x-|-qG>5Gau=iWe|_W&Al952YOJ3GxlhJYZI+M< zqZe;k*w_qy-*yCmpE&o=NOao7qLPNU;uN{DvuM|YQelIyMPNA7W_=EwF(fec(XoQ_ zXR6=aXx~kI6@R4g=|9I0$jjWxZS|UF;F4>+caQkZroYTHIZS(fQbCc z%em8%t=#V6E8wd{o@0zfmn@EOP>!&Yn2`uaASUMc%f3%OKq9G-v;FcoRq{)Fy zCPJ;0aru@@5)sU!i>_<(z2>wuLu04Y*82h9-R#WVVzc8}U+0<39urWIjz0WKA?gjl ztuaVSOso$GfFfBdA>G^n2;v=_zbm&iB*!hr%F2q&^m}WmP(1@c^k{xbXu7Xq_G4YjB8!v@{scjMT)$M7T{@Wvx&;o`LyK6CdG8;d22juwraK z!YMTm)d{A55=B2`e7$7$Cy zl2NLo=KUzEp#KC>wrz3o9$>T6Ffx*G7*YM5p_Fvod%EThC6webT2nGmzkk>Yw*=-| zKmbT?jDU{V`}P7>QP_SHM0vaN}r{#N-fTCYwE^sf05h`xSfh=5xIiy%pUBF0zicw~y21 z&-t3o)Du#)@d?TL1}CpBN3Tvy>Y;=B*JZcD+AC5sajHCb|*lJ z$Pc#UR|2UE3;brwMLsNKhLDj82#}hfrKvga=aC|?B^4yrmseIcHCf&Lq>hnORaJhw z(7L?~MLm5Ec+BD3!0w4(5IPCBM{!N~AW#l5lqw(=4~lsOOe&b^p8{rKDS7E^l3eyt zPNui)CSL&^a()G71ks$HmblcW?aa52YOd%gpl!cjuDBbUEF+@ilR7KPf{pYFQE?}Q zMi#cXkypw@eYfjc>UG7p8r=ihqGIRc@_jZ-8w1IvHFi;x?b}W-0dj0M3pKNAM?+!x9fue$jJEPbO! zf8;*`-WN0QIaV;woqRHP{MeY(xXIy6gA_Ri%ir4(&gwoF2B1XF92^G*2I1+yuJ)UJ zR63#6qKeL2Kkao|4v$9-10xB>eOF#LaMZgUzLXuV{sy3c34SA%D>>~rn4FlieB&tM z<3RD7X~T5Eg?g;|!oqDFwH)s5^)TB-fWLcyfwgf&^2yqe=nb*Y);T;Rno)Nq~xLW^}Z)o&^{dUL#0}3M-Fxbx{Hlgf;u0K-M(SU7W_e ziimjf*kiaPZzsWix3og7OwSWHGMdwF!i=v#JaRc2bzbFy$)NI);N({!Ac zlrZ5|sTaPoJ)LO?a%E%$vngU=9BQ}5r5BOHCRv$|QTNx@0ScN80U_}lmn%6r)yr`R z-$e#2F)<)CFArKGD`%oAuBoZ2N=det8SoVB-q{NpcCAX?-PzfJ!=FF%@{pn`&Us|M zFgn|P0R{y~Nd=<ODP^&e8RlF|_Jy?(&!_=G4p1kI0s`@S^Un<%nHc3zg3PaBK*=_M4+OyOf#Txk z)YRq$z3%mewhAMEu44L_= zK93l{G7O+Eh7j;Hnmv1~#@^BVX{Kys4GFtDdlHC1kma10=d_~FN%8P{^h2uOSD=LA z`~rG^a*Ogzpz#TXeEc@o;G|CqX5?cI3#1^O`+nwS%`jI&bOTxN8%rJ-;EV)PeeQN+ zX=&9SuJ6$W70ao7sX@Hk;nQl`jUKV3(AK-%qS4Gu9(MYZQR&C~jrDAa2s=};(G9>- ziMBr`wo1RV>)qi|5OSJVxOBchdzO6ElKsV|t*x_j5`7QgDE{Bj_MRoC;jHV_2Xv1t zNBhVGabv%g9GJ(+1GK$pguI90zP?t5w!=hHnX9WhypLCx$uE>z&RU0M1_z6ZgjA*G zqxqUVujKW7REK&SXCLgStlzR4TO0}d0Tp}wJHF7d$2@Y1bY4JFxk7%nk!S;7R#(>` zAiy7gJ*as}92i}A3&lg$`8pudJtA}%=&Vw6{(@Adkc?`YnZ3rxzM%KJ{!=Y2c=U?^ zEV`}Ydx#c3#PrjrtHWQEh$y2!Of>{NXmsU!S`M;>g&N#esvau0C+IMS$YZa!t! z6%tSKz+bc2@17s>d(wvG@iGWLUR$f01SYjYzuR0kZBr#Fw{wXuot*tgJupm`WXyshPQML{# zSic=W#N7{H<_1$+dRPHL7{eq9Rq@->11(DVbXk%V8yy;8+B}__v$~Pv4^T6dgZJu- z{>~FWw&Nb#YPXJMtC8^Yn~y_)Y=W3p(UW%I$W7i}K;rIv^SZ73W;87>qfD9r1Q{$X zeYn^>SRDfx4hZV--Aj&{?Kl)~XrOYhFm(pL(yyyY#Dn521hoFY(f*Q|H5^j9*xtgq z35$Mfe>=%tJ3j3dr<3|WPx2|f68L3sy5P?CJuUj}#g2l9MPWS@+6;lng9NouP!3}l zTYo*=#p~v-u)L?-I_W5Hzw=TL@}!9U4=lBK45wud3JY9y{{ z;b35(VV)RVUl1EE8~Jb#RiRAhThY1KBO+>P4rNUr#tr`29&*~(2krwoUweJY@JTL* z_b9|X2h-3HOm&1gNdXIQm`$EpCFz~QQrzI$g0|le1{|f_hT;eMy>60In6*AA+iA>d zpG!ei^<trw;HCGn9L{)d3 zI2s#6(;v1!M3rw6q*(y=YZbx0xiKkjGFQVSx*CBe6vn>Xv)rM$^bIOX9hTSM-W2)X zPmc9j%wHZ14GobrT|JcQOvZlp;&HjNoL5D43>6E-ZM_=eJx$!$+8FqcDOl-KwbN`R zwrvx$`4}LZfh{(Bs&$BEIi2>bc077Kdf!xS*tXseMH@>uIv)X9jEn)U=9*X%#QaUR z$~#=1C?I!nYK-6wxRJ2%LlmNC0Ue8VNH40Uery*YKsmJzD`-zKkkslw$b_t4YhXoK2(K&fevQ;UbTNGq}e;`C29FvxiyRWDwucl7jH#o#lc4I+a z=@wAIYRE?UeKN90#>#E+!yW}=-&pT)PJA5Lh)4*Vh*uMz(C})hwHTL?fzK(U@S|qI zAd_T6vb4_$lK!sJJ>3aCUD|*JTslS$XsO93vw8b?Nh$#8nW6RG{Opvr(^O6kn_3{& zcA_f&UgA;WiOm%Ha(oc~r@6`zo4^_<#G$3sgG~d_T$f-DBv7E$=lfgDHVII_Py2-t z5Y{Q?JU}kMuvHZ)2p9)wr1G9}7!KWKl)eX2c1{j2UcqeuX`Qz6_3&kh3@gB1zS%w+95)$Y=U|RCjahJF;h@|-epsx53C29rqHmq-? zZ^d&d#|E?2?fFB`iq05ADCsHX-)wK00Ki4puczhZYna`#-C^wbZ0eawd1-lh>!x~4 zJa^TR0#pL-U(NgfXIf%u1uitJN6#t|0OCVDZn7JlYe7KsX&z?v;-7t>tAuZE5@F-F zu}x&W@1P393>9PR0Lw{d_Xa=DlP4w=G*4p(O_=InoMCXOHy7$MLk&c_9VfaxS{EaPt_ ztC#sk`5Ox8g{2(9pRSSsaK->B-h5@GG&R6I1ixFb@)GC9OZe!E-wZP_AS|WkU`N6?Jee} zM&ihW!n^!$!6q@giYk2`PEo_)App=a&dl1s+yE`fgBW?qlMxMq>0~(!KP#j{ocG>0 zXJU{L65Nnp+RDebp0sckhGe(pslp!LS`GjT>u;26$6uX24pM&7Ite zeNb`=99--%m$v`bRNP}((EUKI0~lNXH5H3Y?X>+LiHggPgXGgbI@w3*hABlb+xMMw zF5P@Kr!L7yhGqtdUj(Zo4+SR4r$omT^Ra+9gQ9djL$k86$TIh>%q`7;katH183RMa zuQC5%OaR>l>`8vow=p-cX!kDAePSF|`1(C9Z)j9!fJ`V5e3VP);xRmYim0(H1B8Zv zm|byxv4l>})Q8~?(X(LZ)IA1sIfhBNx~R_Z8&9X5AL4{>D&uf8P7~g5*}Ey|XJ$r4 z)ie|Fc9rI)808i1REVxV(TWpNyM4!G+y_WXC+U^J^ZAeor#dC{_6?(4Q^qgNh~w`( zDlKZ)Qr>RHizbe}l%B5O(H(x+G|fBmW&3Amzv8J6ciNB4RvHow%;dD&MnN~5t5WG;FB=6 z7>S68I0kNbq#0YW2TF+M=0dU4HkYhJBElyvs%;D{&L?d=&0h6Le7VCq+W1nnH8kAQ zHtyDEEnlw~w?qKkFG7-``?Ds)rNwd*q!R=&k!pe*RfW92_8$poC z(Ip99znKkyA~d@gzCMiBgp&FQ8`fwzZlS`-SEo^v`E4_`^p1p9rML3&7H&8rk4wLH zT9p>;lAlNsC(u|at8qEniNAR}HNB&RV*2xE@T&CN<(nkCWkj?z*womIj}5R(HZ*AweK8!5A;voMQBt*g^pYr|~f*x1>Q$vRhYD|Cs_UK4WKeA8%QS%tMS zHvW1gQ4~KNMZ=J$P-IQ)X!x8nQtQaf`uSMEp2>idR-huMJIkBg@1BM{cVx|?1BpfW z359Z!<&#sI*HG-QL0c9m=$94d^0WM|Q4ed%_11HCdcJpKZA9o9v#L7QvTL8o>vzkG z6cv>M#MGAEE;es@t~6%ad4g?zhS=7`OT5pqExj5UU6{baBqThrmwb)6_dxAGJ4+D0 z)xXF)C_oq1cp1*Vku^e9TVGGj!a~Er5O>a$B)3#)Wz6>Y2{nR5#pQ$;FnfBhW=H(Z zH1&{FSk-{^;t>#VT=XU4C(Z)gfh^zL8oZZp6cnNq6d-vrxF%cd6Vty0Q^~yxr6LNd zQ4BDUb3HLcBm zHkeqbo!@mRElFw-s`clNT6kL(ja3A~mZ~{9ELd5_roCVD_)FXDCUhJFVQ~+?3Qqrb_CLM=5=N8}P@){ii=Y!im1hTw;U)*VBxw2IY!%k%- z;vRU00^)!0r<~-UM;j!}lXKDti_OjQThf+hOmg|U^24EFV5-?~?^>DJ_1Z45J|TS< z@y0ZE>vI~MJRIIZM#=|NlM=7;a=8-|Y#d0eAwBG+4KkANQVP6hrs0{hHg&GSi!$|) zSvb@^emO$2PoUcYHV51=8#zk9Lyn zQ9vshEwLUUCLK3&9sTUpu#@6U*#H&FOUW0-VPoZueciUiLA@q3DwFVXOhIBE*ITSb zb&qbHwl<A}&#fOX`r)Fitl}gS0}~_Z zG}1S>876QU!RyRg7E)*Oh6r*xj0?}q9ZcS=UAzT7h-Z(jEG(Q~kWdO<$V{Mdb}hI_ zoQ@BhSj|P>_7T+Vh4_m?U*I!G>n42ClQy33;P-A`q<0^Oi5Cs#@#?ej3Ov-FT^Y9U zmZ$sVm|y1t#!;C&}O7hR^T_?Te1<%3;rveP}m&6{=FOuY5JOv!wDv69<>ihjY zf#_hX$t}5{(Y&=pi3iEn%I8v2Wj@u+rZaY%qfv%us->C~dfxW|+XpVZ;V!yfri(wE z>h9LsmgUk}<86*+Dhq^ZWU`qDx`DD&-nW_h`mnMG=!{nIdxCJ6uDctyPZ-n1 zSZf46;}+JIR%RKTc=$v_*hB{yy1H2Xy^w9&NB8kAG&cE(oSYq#fgA>6k@o_T;wHX;1ozsi&j&c|uljG^rnJr)OZ@Bl3k_nDQp5Mca`)!q^Fq`4X&4rU5 z8pZ4L_rG>6>N(>M>$U}eC%&mt1^CkrFO=@CIxfx)=T^ez4;why2B2c-;lgl@*4x3O zO-=;f+O^9@lcBUd4lWj&+rZ@bQMVYI@pMn58<6C127E(SI^I(jg!G@blXw5?QJ2eQ zqZ?qTlgz-bb|Qn8e2=X4TRT?WehHSP({WYYkIT0_9wy)G9e8gKPXF#D*r}-R$uUYzL&v0^~rsU-0T_AE*wTt#KMBGes$3z9RSklZ)D>)!A?7*%Z*fXwBtm(iGmzP>Y5U$M`<*co{#-FBvliUg17I;$(?#e^g~OV?JpMsIy<)u zX=$BTSL-_hCB(X8j{6I1yGIs)cD5{&C}6l2W1cwsVF)|zIXe3Az)+@M2Sjjluic4_ z!ig7JUH45qOD$}Oke|c$XOF_9N-Re*e~9=Fa|N%!e1qL+jmB>)pLwvr751 z!JG}F*h19SRs#bAiAW;Xi?uzA3lB(djv`X$&#o8YodtmnJhwKP&D~2DfMgi=>Dt}8 z#8quoU(R<02W}G?tILrEzhxq(lp3Iipe^EvVQyyTcUkxmb)nY(#kLY#iRnfG$;=0U#xh+_BeFMh$Cn-6(<=V<}$zKWpFCS${vk$(x+`qdk zYursRgt)_3fP z&B%AOrAH8{MBGiY%BS`To{V71i;IeF%BESt#>=Cs9M87m(>&U(*k0gc-tW_OTwE+w z9gK&ZZR0ZP;9%gyv9nE!N>o=9^OZktoMb0{2zpj===~jz8_<>XW_0w8Lezpm3=Q^l zmcMa;I7R%|t>~>guQBJ#1|ARiPMj?+vX-U5S&fnkI(h#kO<4dUHxbszBID1Z~{-a zohKk7 z0AnuQ)s1|7>?&fU<^4>X%1G_LTA!!34UAUgvaFR)c6R?`> ze|{1?sWOSuDyTm5Afw*1)~NrM*GqEAP}2eXp37(&mIFdU5B{f>MhZfZOY}2#tv0qs zomPh4vxv^rD@kEbC0~xye(pTVecjB)t>N8c(7hh#DeCcaGY={&vkxNT5%|fmymWk| zYYTA5jYP!6^pw5Cjh!N14@($RaPlE^dc4X}k(Fh4&mwdvg_$6Hf&PR7(oSFL^&IN; zE9ISd_GsY*L5-%Y;X4905&{f${v?m_;`A4trEr611N;>Z53U{zFb9K~DaPmT)1*l+ zQxx!%_9z2PC=05h>s`Z!RAxgiYUNv4Q>kio&#k+Wzx~ri^pHZ^?+C| zI+1|Rl@uRL{wC@AWJDszjG&*Z3c-DK5nq|dzm<6L9E$a^@+IxFNUw*TPk8zy{VYSD zqXo%D&DI3{c^Te9Y|{C8NJBr{nhEuSgULVKf`J^d-^E+M+kz0uXi(Ay!ntqQ)!xha zZ=)j$F9xp{i!Hq<-+ua;ecGg#%V6ihc2SB<`+yq9x!l_5_-7@jN= z?=qYp_3Rx7?esS(s3HX6{esG>&7UMRAzwh*ov}?l72`x{SnSqg9S2Jh3~`P%Xf+=W zQ>>#mNpEw*!8bMy8H}^l3_H|&`nf-O){0zv7nkXgBIA|fXe6Z)?cYc9fb@jovtYeT zr+wn)U`>w#mO_Xj7afq-bAy@6Okrsmp8xU6;e}3_7t|GUg^yixK6ELhSk?Vw*$YG^YpNVZek|3OtiVESu@bi*XxVUtk zb`79B0X{;HzkhfhQ=hDP9A`GyO3AnA#&&Fl5X@!`?f|T*Y~_^p?nEyesb=3y_)#o- z$qvzK->0hkW|7ua5zKX#PU1J@2zi4B%xs+FB=7Wjh~Q^CsX4AdnQQM{;LxW(r?^K0 zeCL9Wzzw0z+W0E2$~~&pN3}@>iDi-F&c!m2h#s!R<> zejpH$LSXD0j^}#&Di(~5fgy5R{(a=52MDf1&q)9N&#_02o(=RVA$$o7CDLel8{PAq zLNIimJ3RqgL_)kY21_1;z=c*0vi*gZmN~etYR;muHHSsEXb2URC>s@@5GN^X@_WCO zbMi&PsMCO%fIeGntwv{cyde3|kqKTW>pyn&mX?;ZwDeNLJuhE9&n&}+)HAkp2#@!! ztbA%v(9|{(``4QO@DGr|i|+8eBc-b8<~%A^H61I20{?xRk$*He3wZP&+M{E_SaZn_ zwydD4TILXaCCHa&uX0nnNjb5jKd6&lzFMPf$9#A+Sbs+tE znv0TRF~yD_UQFbKDfY166fAhXl<-H8|3=+RpS6>8SRdf+U8Vi~&ZU1h-JsWu9^7G(FfGcv+Te_2@FmcwXfV?)D8-!@?L``n)&fNbi|k3C{B299sJ z1C%F}FieRka82%DmKap{^$J zBAUH4eV~e4a-l}Ef7G?zRB`3R{NYZT*5%?wDh0|vTkCcQIrPQpT;=U%Zasb52~4^`c3Ch7f}MwddKN-sHzH&`mleUz)hnN-EVuslpYkP=DbU zFu!{9lq-;H?^%FTguO;mR2h!Zt(b9!bQ~2wrFq`Wv33}x26F{gI%NS~EYsL&+E|QR z!KI>HFz@ww!ZOze)=?~bg&sQ zp5>iUiZ$#8g+{=KImrmm{y@qU$-)z!V`=-===!5?@?5OHtP~+ARknvl0aR z_jva>K=JV5L>ry0m?R~=I6O(=#du6-Afp2gJd8S6`y*8uw5QM9ctr)-#UWB7E2Zvm zfFigWEAO=|c)THa+9|p&7eeF(9yt29_IZc?_ZuORsNi6p<0(#!g&0yqdx{;~m!QZc zZS#GdAiqMl_9mNsXc?nPMrON-iYK7Eb=}MVS-#`HoDQe_OFuD3StwFl1HI}nuqoeui?Ts{~kw( z5vaUhv9)n*zJX$H-WyOu+5Gi9MibL3Z0;NIv+(94CFQ>`!F2Fh`ed7RvZP3H<#^;J z4*v7)PrnDddme@?@-%$t#4~XXFg3?94#-V<6)HW~<+AV=G~!NI+8a_&eu5#}Ah$=^ zk_VvzTGXG!K89ml%n`b7>rd-sr-$>T)*mDk*!2(@}-~lQ%x-!L2IZ} zM{_J!$^Vr2|Nidle|`_w1o3o(o-$G~Qc%88t+bg%L6BR&_O96GXVoZajA64%U<2mw znGnvAexC}QCl=#hHzbb3*;ySP8$y9SSe#0h=z9cs%l0+6kbJ-X^1^HiqDpc zk^j4@0lqts5Ulp^JOMb~=cVIE;LqcuLE7g?;2-4Q^J_!|J;uQOF}ID`-)EE(lM^i! I`SkU_049XjSO5S3 literal 0 HcmV?d00001 diff --git a/modules/agent-box.nix b/modules/agent-box.nix index f1701d2..6d4abb1 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -3075,13 +3075,41 @@ in .tabs { display: flex; align-items: flex-end; gap: 2px; flex: none; padding: 8px 8px 0; background: #010409; border-bottom: 1px solid #30363d; overflow-x: auto; } - .tab { display: inline-flex; align-items: center; padding: 6px 14px 8px; + .tab { display: inline-flex; align-items: center; padding: 6px 32px 8px 14px; font-size: 13px; color: #8b949e; text-decoration: none; border: 1px solid transparent; border-bottom: 0; border-radius: 8px 8px 0 0; white-space: nowrap; } .tab:hover { color: #e6edf3; } .tab[aria-current] { background: #0d1117; border-color: #30363d; color: #e6edf3; } + /* Close (x) button, absolutely placed over the tab's extra right + padding so it reads as part of the tab while staying a DOM sibling + of the link (see render_tabs). Dim until hovered — always visible + rather than hover-only, because touch has no hover. .arm is the + armed state SCRIPT sets on the first click: a red "Close?" pill + growing leftwards over the name, so the second click is clearly a + confirmation and never mistaken for a plain tab switch. */ + .tab-wrap { position: relative; display: flex; } + .tab-wrap:hover .tab { color: #e6edf3; } + /* The armed tab tints too: the pill necessarily covers the tail of a + short name, so the tab itself has to say which session is at stake. + Only the (already 1px, transparent) border changes colour, so + nothing moves under the cursor between the two clicks. */ + .tab-wrap.arm .tab { background: rgba(248,81,73,.12); border-color: #f85149; + color: #e6edf3; } + .tab-close { margin: 0; } + .tab-x { position: absolute; top: 6px; right: 6px; bottom: 8px; width: 18px; + display: flex; align-items: center; justify-content: center; + padding: 0; border: 0; border-radius: 5px; background: transparent; + font: inherit; font-size: 15px; line-height: 1; color: #6e7681; + cursor: pointer; } + .tab-x:hover { background: #30363d; color: #e6edf3; } + .tab-x.arm { width: auto; padding: 0 7px; font-size: 11px; font-weight: 600; + background: #da3633; color: #fff; + /* Fades the name out under the pill, so the overlap reads + as deliberate rather than as clipped text. */ + box-shadow: -7px 0 9px 3px rgba(9,12,17,.92); } + .tab-x.arm:hover { background: #f85149; } .tab-empty { color: #8b949e; font-size: 13px; padding: 6px 8px 8px; } .tabs .btn.add { margin: 0 4px 6px; padding: 2px 9px; } .tabs .spacer { flex: 1; } @@ -3619,6 +3647,79 @@ in wsSelect(t.getAttribute("data-tab"), true); }); + // Two-click close on the tab bar's x. Closing kills a live agent and + // the button sits a few pixels from the session name, so the first + // click only ARMS it (red "Close?" pill, see .tab-x.arm) and the + // second lets the form submit for real — a confirm() dialog would be + // both heavier and skipped entirely when scripting is off, whereas + // here no-JS simply keeps the plain one-click POST. + // + // Anything that could leave a loaded gun on screen disarms: a + // timeout, Escape, tabbing away, arming another tab, or any click + // elsewhere. A tab-bar re-render (polling swaps #tab-bar wholesale) + // detaches the button, which the isConnected check absorbs. + var ARM_MS = 4000; // long enough to read the pill and aim at it + var SETTLE_MS = 350; // a double-click is not a considered confirmation + var armedX = null; + var armedAt = 0; + var armTimer = null; + function wrapOf(b) { return b.closest ? b.closest(".tab-wrap") : null; } + function clearArm() { + if (armTimer) { window.clearTimeout(armTimer); armTimer = null; } + var b = armedX; + armedX = null; + return b; + } + function disarmX() { + var b = clearArm(); + if (!b || !b.isConnected) { return; } + var label = "Close " + b.getAttribute("data-close"); + var w = wrapOf(b); + if (w) { w.classList.remove("arm"); } + b.classList.remove("arm"); + b.textContent = "×"; + b.setAttribute("aria-label", label); + b.setAttribute("title", label); + } + function armX(b) { + disarmX(); + var hint = "Click again to close " + b.getAttribute("data-close"); + var w = wrapOf(b); + armedX = b; + armedAt = Date.now(); + if (w) { w.classList.add("arm"); } + b.classList.add("arm"); + b.textContent = "Close?"; + b.setAttribute("aria-label", hint); + b.setAttribute("title", hint); + armTimer = window.setTimeout(disarmX, ARM_MS); + } + document.addEventListener("click", function (e) { + var x = e.target && e.target.closest ? e.target.closest("#tab-bar .tab-x") : null; + // Second click on the armed button: fall through with no + // preventDefault so the form submits (the submit handler below + // upgrades it to fetch + patch, like every other form here). Only + // the timer is dropped — the pill stays red until the patched tab + // bar comes back, so the click has visible effect in flight. + // Too soon after arming it is the tail of a double-click, not a + // decision: swallow it and stay armed. + if (x && x === armedX) { + if (Date.now() - armedAt < SETTLE_MS) { e.preventDefault(); return; } + clearArm(); + return; + } + disarmX(); + if (!x) { return; } + e.preventDefault(); + armX(x); + }); + document.addEventListener("keydown", function (e) { + if (e.key === "Escape") { disarmX(); } + }); + document.addEventListener("focusout", function (e) { + if (armedX && e.target === armedX) { disarmX(); } + }); + // The editors render expanded (no-JS fallback); collapse them once // JS is live so the page opens in list-only, GitHub-style form. ["secret-editor", "session-editor", "password-editor"].forEach(function (id) { @@ -3973,15 +4074,31 @@ in """The workspace tab bar. File order, not sorted: sessions.json preserves insertion order, so a new session appears as the rightmost tab, like any terminal app. The dot-only .state span - reuses the list styling (its ::before is the dot).""" + reuses the list styling (its ::before is the dot). + + Each tab carries a close (x) button posting to the same + /sessions/delete route the settings page uses (no back= field, so + it redirects to the workspace). The button is a SIBLING of the tab + link, not a child: a inside an is invalid markup, and + keeping them apart also stops the tab-select click delegation from + swallowing the close click. Closing kills a live agent and the + button sits a few pixels from the session name, so SCRIPT arms it + on the first click and only submits on the second.""" items = [] + base = html.escape(SESS_BASE) for name in names: safe = html.escape(name) cur = ' aria-current="page"' if name == selected else "" state = "live" if name in live else "starting" items.append( + f'' f'' f'{safe}' + f'' + f'' + f'' + f'' ) if not items: items.append('No sessions yet.') diff --git a/modules/src/settings-daemon.py b/modules/src/settings-daemon.py index 126c4d7..d664f8d 100644 --- a/modules/src/settings-daemon.py +++ b/modules/src/settings-daemon.py @@ -896,15 +896,31 @@ def render_tabs(names, live, selected): """The workspace tab bar. File order, not sorted: sessions.json preserves insertion order, so a new session appears as the rightmost tab, like any terminal app. The dot-only .state span - reuses the list styling (its ::before is the dot).""" + reuses the list styling (its ::before is the dot). + + Each tab carries a close (x) button posting to the same + /sessions/delete route the settings page uses (no back= field, so + it redirects to the workspace). The button is a SIBLING of the tab + link, not a child: a
inside an is invalid markup, and + keeping them apart also stops the tab-select click delegation from + swallowing the close click. Closing kills a live agent and the + button sits a few pixels from the session name, so SCRIPT arms it + on the first click and only submits on the second.""" items = [] + base = html.escape(SESS_BASE) for name in names: safe = html.escape(name) cur = ' aria-current="page"' if name == selected else "" state = "live" if name in live else "starting" items.append( + f'' f'' f'{safe}' + f'' + f'' + f'' + f'
' ) if not items: items.append('No sessions yet.') diff --git a/modules/src/settings.css b/modules/src/settings.css index c27af08..f85cbb8 100644 --- a/modules/src/settings.css +++ b/modules/src/settings.css @@ -101,13 +101,41 @@ body.ws { display: flex; flex-direction: column; height: 100vh; .tabs { display: flex; align-items: flex-end; gap: 2px; flex: none; padding: 8px 8px 0; background: #010409; border-bottom: 1px solid #30363d; overflow-x: auto; } -.tab { display: inline-flex; align-items: center; padding: 6px 14px 8px; +.tab { display: inline-flex; align-items: center; padding: 6px 32px 8px 14px; font-size: 13px; color: #8b949e; text-decoration: none; border: 1px solid transparent; border-bottom: 0; border-radius: 8px 8px 0 0; white-space: nowrap; } .tab:hover { color: #e6edf3; } .tab[aria-current] { background: #0d1117; border-color: #30363d; color: #e6edf3; } +/* Close (x) button, absolutely placed over the tab's extra right + padding so it reads as part of the tab while staying a DOM sibling + of the link (see render_tabs). Dim until hovered — always visible + rather than hover-only, because touch has no hover. .arm is the + armed state SCRIPT sets on the first click: a red "Close?" pill + growing leftwards over the name, so the second click is clearly a + confirmation and never mistaken for a plain tab switch. */ +.tab-wrap { position: relative; display: flex; } +.tab-wrap:hover .tab { color: #e6edf3; } +/* The armed tab tints too: the pill necessarily covers the tail of a + short name, so the tab itself has to say which session is at stake. + Only the (already 1px, transparent) border changes colour, so + nothing moves under the cursor between the two clicks. */ +.tab-wrap.arm .tab { background: rgba(248,81,73,.12); border-color: #f85149; + color: #e6edf3; } +.tab-close { margin: 0; } +.tab-x { position: absolute; top: 6px; right: 6px; bottom: 8px; width: 18px; + display: flex; align-items: center; justify-content: center; + padding: 0; border: 0; border-radius: 5px; background: transparent; + font: inherit; font-size: 15px; line-height: 1; color: #6e7681; + cursor: pointer; } +.tab-x:hover { background: #30363d; color: #e6edf3; } +.tab-x.arm { width: auto; padding: 0 7px; font-size: 11px; font-weight: 600; + background: #da3633; color: #fff; + /* Fades the name out under the pill, so the overlap reads + as deliberate rather than as clipped text. */ + box-shadow: -7px 0 9px 3px rgba(9,12,17,.92); } +.tab-x.arm:hover { background: #f85149; } .tab-empty { color: #8b949e; font-size: 13px; padding: 6px 8px 8px; } .tabs .btn.add { margin: 0 4px 6px; padding: 2px 9px; } .tabs .spacer { flex: 1; } diff --git a/modules/src/settings.js b/modules/src/settings.js index 08e75c3..2d091ae 100644 --- a/modules/src/settings.js +++ b/modules/src/settings.js @@ -290,6 +290,79 @@ wsSelect(t.getAttribute("data-tab"), true); }); + // Two-click close on the tab bar's x. Closing kills a live agent and + // the button sits a few pixels from the session name, so the first + // click only ARMS it (red "Close?" pill, see .tab-x.arm) and the + // second lets the form submit for real — a confirm() dialog would be + // both heavier and skipped entirely when scripting is off, whereas + // here no-JS simply keeps the plain one-click POST. + // + // Anything that could leave a loaded gun on screen disarms: a + // timeout, Escape, tabbing away, arming another tab, or any click + // elsewhere. A tab-bar re-render (polling swaps #tab-bar wholesale) + // detaches the button, which the isConnected check absorbs. + var ARM_MS = 4000; // long enough to read the pill and aim at it + var SETTLE_MS = 350; // a double-click is not a considered confirmation + var armedX = null; + var armedAt = 0; + var armTimer = null; + function wrapOf(b) { return b.closest ? b.closest(".tab-wrap") : null; } + function clearArm() { + if (armTimer) { window.clearTimeout(armTimer); armTimer = null; } + var b = armedX; + armedX = null; + return b; + } + function disarmX() { + var b = clearArm(); + if (!b || !b.isConnected) { return; } + var label = "Close " + b.getAttribute("data-close"); + var w = wrapOf(b); + if (w) { w.classList.remove("arm"); } + b.classList.remove("arm"); + b.textContent = "×"; + b.setAttribute("aria-label", label); + b.setAttribute("title", label); + } + function armX(b) { + disarmX(); + var hint = "Click again to close " + b.getAttribute("data-close"); + var w = wrapOf(b); + armedX = b; + armedAt = Date.now(); + if (w) { w.classList.add("arm"); } + b.classList.add("arm"); + b.textContent = "Close?"; + b.setAttribute("aria-label", hint); + b.setAttribute("title", hint); + armTimer = window.setTimeout(disarmX, ARM_MS); + } + document.addEventListener("click", function (e) { + var x = e.target && e.target.closest ? e.target.closest("#tab-bar .tab-x") : null; + // Second click on the armed button: fall through with no + // preventDefault so the form submits (the submit handler below + // upgrades it to fetch + patch, like every other form here). Only + // the timer is dropped — the pill stays red until the patched tab + // bar comes back, so the click has visible effect in flight. + // Too soon after arming it is the tail of a double-click, not a + // decision: swallow it and stay armed. + if (x && x === armedX) { + if (Date.now() - armedAt < SETTLE_MS) { e.preventDefault(); return; } + clearArm(); + return; + } + disarmX(); + if (!x) { return; } + e.preventDefault(); + armX(x); + }); + document.addEventListener("keydown", function (e) { + if (e.key === "Escape") { disarmX(); } + }); + document.addEventListener("focusout", function (e) { + if (armedX && e.target === armedX) { disarmX(); } + }); + // The editors render expanded (no-JS fallback); collapse them once // JS is live so the page opens in list-only, GitHub-style form. ["secret-editor", "session-editor", "password-editor"].forEach(function (id) { diff --git a/tests/e2e/root-sessions.spec.ts b/tests/e2e/root-sessions.spec.ts index 6354248..129f5a5 100644 --- a/tests/e2e/root-sessions.spec.ts +++ b/tests/e2e/root-sessions.spec.ts @@ -123,6 +123,62 @@ test('add a session from the tab bar, switch tabs, delete it on the settings pag await expect(row).toHaveCount(0); }); +// The tab bar's x kills a live agent and sits a few pixels from the session +// name, so it arms on the first click (a red "Close?" pill) and only closes on +// the second. Covers all three legs of that guard: the first click is inert, +// the armed state gives way to a click elsewhere and to its own timeout, and +// the second click actually closes. +test('the tab close button arms first and only closes on a second click', async ({ browser }) => { + const page = await authedPage(browser); + await page.goto('/'); + + // Add a session to close, so the test never touches "main". The name is + // auto-derived by the daemon, so take whichever tab is new. + const tabNames = () => + page.locator('#tab-bar .tab[data-tab]') + .evaluateAll((els) => els.map((e) => e.getAttribute('data-tab') as string)); + const before = await tabNames(); + await openSessionEditor(page, 'New session'); + await page.locator('#session-editor button[type="submit"]').click(); + await expect.poll(async () => (await tabNames()).length).toBe(before.length + 1); + const name = (await tabNames()).find((n) => !before.includes(n)) as string; + + const tab = page.locator(`#tab-bar .tab[data-tab="${name}"]`); + const x = page.locator(`#tab-bar .tab-x[data-close="${name}"]`); + await expect(x).toHaveText('×'); + + // First click only arms: the session is still there, and the whole tab + // tints so it is obvious which session is at stake. + await x.click(); + await expect(x).toHaveText('Close?'); + await expect(page.locator('#tab-bar .tab-wrap.arm')).toHaveCount(1); + await expect(tab).toHaveCount(1); + + // A click elsewhere disarms — a stray first click never stays loaded. + await page.locator('#tab-bar .tab[data-tab="main"]').click(); + await expect(x).toHaveText('×'); + await expect(tab).toHaveCount(1); + + // A double-click is not a considered confirmation (SETTLE_MS): it arms + // and stops there, rather than arming and immediately closing. + await x.dblclick(); + await expect(x).toHaveText('Close?'); + await expect(tab).toHaveCount(1); + + // Nor does it stay armed indefinitely (ARM_MS in settings.js). + await expect(x).toHaveText('×', { timeout: 10_000 }); + await expect(tab).toHaveCount(1); + + // Second click while armed: the tab and its pane go away. + await x.click(); + await expect(x).toHaveText('Close?'); + await page.waitForTimeout(500); // past SETTLE_MS, well inside ARM_MS + await x.click(); + await expect(page.locator('.msg')).toHaveText(/Session deleted/); + await expect(tab).toHaveCount(0); + await expect(page.locator(`#panes .pane[data-pane="${name}"]`)).toHaveCount(0); +}); + test('the working-directory picker suggests folders one level at a time', async ({ browser }) => { const page = await authedPage(browser); await page.goto('/'); diff --git a/tests/sessions.nix b/tests/sessions.nix index 2cd6f9a..7ec9b30 100644 --- a/tests/sessions.nix +++ b/tests/sessions.nix @@ -419,6 +419,13 @@ assert 'src="/agent/?arg=main"' in root_page, root_page assert "workingDirectory" not in root_page, root_page + # Each tab carries a close (x) posting to the same /sessions/delete + # route the settings page uses — a sibling form, since a
inside + # the tab would be invalid markup. The two-click arming that keeps + # a stray click from killing a session is client-side (tests/e2e). + assert '' in root_page, root_page + assert 'class="tab-x" data-close="main" aria-label="Close main"' in root_page, root_page + # The root page's CRUD routes (behind auth) can add a session; the # workspace redirect lands on the new session's tab. The name is always # auto-derived from the agent (there is no name field in the form): with