From d6024b3ca5343d6a1aeb1abc776846f0dcee8ba4 Mon Sep 17 00:00:00 2001 From: Forgotten Date: Wed, 18 Feb 2026 13:02:23 -0600 Subject: [PATCH] Enhance UI: favicon, AgentDetail overhaul, PageTabBar, and config form Add favicon and web manifest branding assets. Major AgentDetail page rework with tabbed sections, run history, and live status. Add PageTabBar component for consistent page-level tabs. Expand AgentConfigForm with more adapter fields. Improve NewAgentDialog, OnboardingWizard, and Issues page layouts. Co-Authored-By: Claude Opus 4.6 --- ui/index.html | 7 + ui/public/android-chrome-192x192.png | Bin 0 -> 8982 bytes ui/public/android-chrome-512x512.png | Bin 0 -> 29374 bytes ui/public/apple-touch-icon.png | Bin 0 -> 8062 bytes ui/public/favicon-16x16.png | Bin 0 -> 669 bytes ui/public/favicon-32x32.png | Bin 0 -> 1609 bytes ui/public/favicon.ico | Bin 0 -> 15086 bytes ui/public/favicon.svg | 9 + ui/public/site.webmanifest | 19 + ui/src/App.tsx | 1 + ui/src/components/AgentConfigForm.tsx | 180 +++- ui/src/components/NewAgentDialog.tsx | 30 +- ui/src/components/OnboardingWizard.tsx | 26 +- ui/src/components/PageTabBar.tsx | 19 + ui/src/components/agent-config-primitives.tsx | 5 +- ui/src/pages/AgentDetail.tsx | 810 +++++++++++++++--- ui/src/pages/Issues.tsx | 36 +- 17 files changed, 982 insertions(+), 160 deletions(-) create mode 100644 ui/public/android-chrome-192x192.png create mode 100644 ui/public/android-chrome-512x512.png create mode 100644 ui/public/apple-touch-icon.png create mode 100644 ui/public/favicon-16x16.png create mode 100644 ui/public/favicon-32x32.png create mode 100644 ui/public/favicon.ico create mode 100644 ui/public/favicon.svg create mode 100644 ui/public/site.webmanifest create mode 100644 ui/src/components/PageTabBar.tsx diff --git a/ui/index.html b/ui/index.html index 8b5d8752..8f0a47e3 100644 --- a/ui/index.html +++ b/ui/index.html @@ -3,7 +3,14 @@ + Paperclip + + + + + +
diff --git a/ui/public/android-chrome-192x192.png b/ui/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..32120f8a74b233aed7504f6ef20a93e5207b82a1 GIT binary patch literal 8982 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 literal 0 HcmV?d00001 diff --git a/ui/public/android-chrome-512x512.png b/ui/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..a7493ed46e367723edc6daebe3b50014677bf07c GIT binary patch literal 29374 zcmc$`c{r78`#-$QB0~}~OGyzbA<3*1qLDI7Q8Z~8GfR>}*)k<0BxNWiGnJ6Aq@pAu zS*8#&^M3AS@8`Rp@B8~5@1JkaaXfok>t6SLUFUV4=Vv(YP?LSTIoJf*NF)-+9(~;d zBoaOTmY&2){N<9M&SU(A+0J0ME@_eYUvgkrhH>d^hFmJ&R1{Vgs))@6fY7Lp^^f{ zS?QUNk{GqPEqVX{{R_LQzc}k-8wG*ev_Dc(Qt9dG`2RO=-Y_#WU%7HcmM`p{w2qFB zRMhA9@85s@YPkQR0Frn)0Kd}kl$=H}kJN3R+} zp)J0Xl9Cc0#@*drY^Rsv<0}heUrR3);4{g@pU%GK49fP`VUbLl++=$P2jvT0uQLy3 za0)41ue8zZExCqud-?L^@#=7qonGUWK`i+2R^r2oc4yA)y!2;q_12T)11(%dY3p zzFQr=mnl()xMs5RnKS&dW}SU~r<|Rax4*xz*TBGiur<$p_#1YOdRy)Wp5K47BSlyPMmUvnfo$+M@$G z^}C4=7(|Hfye)6N@T+*ZpBg!vEqUqpk6MI<^0{x1%8qVG;jpLU z)}6&V&5t)5YfS%GxpU`EDqqRrLx-GObHD$njiN{-u8gYxrEMyI%_h@QX+}VCnq4W}0BI5U^ z{`e~{jr?cNv8@KQ!IU_^acWx^0c=g>K?!TH+cT=_}GU5b} z7L89#1O)~n60Tpro@}nVT}UY5^HT>6@2N;{%VbMp^Kz=Hs`S?K+z`{aTQWK<;N|7D za{WFsnXGp4=kh{L>b9H2XNaiI%*_1x^Cv~Zk_i5Haa+T}!iGD_x!MCaKOwPp9TpH2 zd{b5BGt-yxL(orp*REZ|C1d-XoSgRWUqNQiCeEI{&&>FWP~GEit_N`T_2VbLe*HQ) zG-RBl{@6r1`QnHUmOOcMWTd8{0nflfW*uF>dw2D+l~Q-qJP*jl=oHvo5iaFnq(>0_ z^%pM=Kb5;njCr|0uIJcEetv!`_q#|*QXqvjs&+XChh?4})Al}NW8@0rMm#;I>l2hy z%yYHyBPDlIQf!YM`+|o^(YeZRyCp7ay|M4_AA2rb*i2>@C88(B=ILkrN!=ZBg;T;R zywav*^o`KW%nUXx;A|y{SlcyN4W7`;cP!r2;Xv2d*Bd9QrX9#kHg_b#-9wI@ojp`{ zi%kWI%1s}KNE`lE8X>Y{*ICg?dW6mEr=P!kL3YW*<5MJF%eauq`2__UUgOfA=^BZZ zPz!qY?3tm|z{f0pti-XI$>DPj4({XB{VsCUIN}!lp62F8?oIpi=MP0fcZYz2jsLS} z6QkYLDLT}vxPd7aq{GZZ{hgg85$${-2Z!EY?ZrvzK7y*3-H5N3XJ+`7`_3opTy;|- zQ%!=kFD%S?Qn|f|6SnZ7^pg8ceckNozDK(vL@oF4zc4@JO63+L{-gA@d#%pTb=Ry` zbmS(prfkBIeR+8qznsMj_hC^oyJci?JU3(2$){Czo8uM|MZ>E0?%5+DHoS-=crWYt z)6d!|k=`u@VG5f!*S>qFs3hf}E`99|{hFc|XWNUGG18lwno_y%b;riWE*DlAsR&^B zQAdaMvWy7`2+$N4K){n(o0_~P`qK_=Y)st2B0_&17Y=*h($e_u-P*~i!6&=Ig*`{R zs*2r*wL>)RT3cJSl(}_xH#avM7#kyEp*p*~A4HtZ?r=4%Tcy8kl(wiPz|g~ye3Y&x=uJPX!3=fKx*ph z+CruhWkgj~)$Iw(y+jSaZ*6VGb>B9HloMTlktRd@P+PG=cPPaibTifr! z?EJ`P&nC4`o;-P$_GhJ(p~ALpAHT7?-X9zsM4aoGR*19vQ?<=xkGQxz2?=2#>(5GW z+SFcfuH*Zwkm0X|&6V=?_FHy`3vWO1_Aiu@Zr?p=+9^{gLeJB8by-l}n-8`t|EoRcF3DbMk12#XI{yW>F-D_GBKq ziwZy9*POcR#+J>S0~$O-=@^)WHGE6Lgc*sQ)wYmx|NP_xWtC%meEh9jw=Qej9z3{` z9CqZxhYy3_O547C`BGcE4C^eg{7PbC`?u0dDLSh3M@hO4PhEQ6_SD7bm?m$Km!F)N z*y%H)NWSW}f%n?n+#GHgQQtHzda=}NqKMpgz|(U(`RXTPH#gSZ-ZVBk8j!7dac2CK zXoTtE!%QOYwX&_teP_n|km3bauhuG1i;0SAj?h@R+EQhji`b(;wyJt19fsAz89s-`9!BQLz47ioOT zd*S(+wqzagXV0H6Ac*E?C*9AV_dn~&j3yIn)>d#%C&Y0D3c;VB^$Gl|Rs}bWn{JH0 zaNz=WF_1H$g=qb1Zes0rsH$ogs0Gc4lZv#%a4tsvpq)T{e7`8P%kPwvQ&>dA;&g8k z()PW_kB3_Gj*qKxqRpdzjbXsj*$fU9bcBb7qDWCB;tymVs;#ZXDZIj&!as4r|LN1M z9K!;(Cr^F~Enn$hKPweD6@$)h?b@}S+vZzxtUC&gHO0z{M~(DDQ5a~+ z5hDjbNBGejpz!U}*H0dc30=@jJCK%^=Ce3INs$;wavd8R!+IHJwdpAcESHM%oSV8p z<$f%_o5bj=rl$5O$2y{EJVScp&YeEJ4T+Rh<*inHh*9CZU||Qe*Lds zzxw+65bYER*4K8UG1k7zECZsDtC1&)ii)&D=IUR*ym!#oLuVh|iq?t?Grd24IDg4s zRz$C{=H}HSVyruM?BEhmAQj!4$NF{JPK|s=>*8UwxXJwaj~_oYO>Oq1nCR;1A*bV7 zXZ{R+rYswZz%MgM?z&F&9-kfyky!(gM%fwZschGgd?@`~=YPJ7^RRSJUWR6;2x-~u z_o zld0DlH0M7S-oDL8jnpu@tF~G-B@s7DX-U=!Li>q4nWDozhl3p&8k#sO`cfqG(WC3? zrW7v^4;ymJ1|#yrMS*);FSxkyumw9S38^w_Sy@@RxNM1A7;?fXQ1sL-4i08|EVs|c z=f@=yqsVquRV2cL2M>qcM-1ZGC?4+db=E7PKEsbR9rODg1_D7B!!PfpB8F(piZO)-=V9+z{Epr<&4^JVgmkOiVnqvC3&iY=@idnl*uji=4@aHlA|X9+{9J zMAj}wH>m==nE|bEhc>s(Z$joenj0)S^0$p31BVGRH$wj(P#xGKX_2n(XmLNGYQKkJa0pi2A5@%C!eN6 zaiK`Simh9>W*)lMva%rT8j&*Zs(I3j{3d!~K&VZdHr2m-hnkJOvb3~BXX4|tlN`Sb z%fIV}AX6KGSxWd;u3VXCcD-f1++);EQzd@o+yYNJzZ)7sie{uNCXE^;51i{8t^ zDa|d2EMQfq$@my^%%=BpFEx)5%}c>KyR5DKfC6(l^z`(Q#6r$^_+UdsuGOD(aB$!f zRTEHUth^zltRmd-DufG%#q8+OGp)I{wYS#gT-@=|s?^KV%WF&AJ8T-IpDzPnFw1vb zF<3Qq;=~E`?{v!@`G0H_zt@d?!p2BnqOK{kX;Y6&A$e-y=FeT%1@eIW-3rgF4y!tr zXBTWDeg9+W^y$;5D~35*%KiLW{oMc|nzWWLU%qAQ*3qKG8FV!&!kiq90VVz+At7e6 zyHtC}4glm#15rPI{J3q@p)CF7OYK%tNw%f0C`wIua!yXt-Mi{8KlE`VdwP1*)z!&m zqCd;ijS_C(x%23Tj6M3xV${+X!XjEz^Nl6`RYik1yF#^#zlftU*eUxa*!f~O<@r6al$6sqX zuDDS~#>d?rB- z^IWq>69X+l);Tv&3$DESe(2C4%@FbP?(QAAwmKn+5hrQA6Io^6XY8j@D@jO5sMWs( z7(8W_q10A&b%3*49NYf>eih+;M~|*XSOM(*p7!3kvozd$|D{W_o?|~oQh8N z*!g0dKuBMb!o&ADI}hfcYQ_Sg=3FfNN-Nvq=UrWm3=9~i54iy+bh6Nas0ijr{^%}g z#epto2iT#bsNytu$gQZTc)rkL_?nFwi1d{Mj6SbOjU9C!XM5*{NZknL(quQBukoT~;Is2^IKAhtU({ud9 zH?LIMTn-7j5|n_5*ly7U+14Q&)6mxet8e82Q-7glC&& zUB2An#d!%bQy_{vBO{|liN^}uuRk5bYr6}y+Oxp8pv%%~p8rz)uXKVT$Y+p?DRyjH zLuF$$GBN^dhEs`~L*FLJ%XZ53TOn32mk$5QKZny-sMi3 z-to3`A&tqdYBb9l55w(&~!t{zFbqGM!mZ&1736Dy^NbgJ8*W^`{@QV-;Mp^Rm;@Tb zFQ7=RHo~6xLonb}1x{sH$v#eFK#`f*om6iGN?0I>4 zX0k{a8sw|HIE7V6LN=#PEt|EpKuVLd#Q{a{i;Jl09^4oWv^oTI=g+BCaq?-fc1l## zN_=VK#*J+3?6yJ`IpM*j{HKb6Q9YJ%aYZ0Abi1}ifuce#)ixCe_p;_@O;r^Wu7xKB z8qeh*3Nr_qPu=wd)Ai$reM!2Xo7*4d4sQm_fQp(LX&D(ObP?#0k=W3T@spXD7WUMF zk+q?vpw|}*i;FYRhi;p%vKvLqj#wZMeA={i>qV3fqP0%4N`19%q*Ls+zed#W33Oy? z31~Bt8mkAwd$9%yN@sx5Y;0^Ig&^L_blVs9QO{@&?r`c4qr7gx)Bj!tf$9BuIYFaF2XKka_ktJ z5Je8Db^#S&?M;2^Sj7dSIdGwS_UsX8k4mhye^XsuKL4i;k&4{?!U7bOdy3A&$rC4- zd^x0%k9t$?E&$cyzq<{8ON@XPH&=BKp3~n>Py>`7sCOfrTo(*o&;~F*mh2qFQFdh5$}aW-czS zk>T~i*f9BS8C5 z1q@>3v_jMp9zWhRrA(5(woy**%05RimG<5+M|tpAh(!O76ZCfNB~50x;x&%XB7Dw!jirs5{jyx+fn?~L&9-YgZhCrWZ-{GyhG1bcFp!q%-(`fI_~ z1aWGOREzmd&CY6g{$@n6yQddMPft&<0~w3xk&|_-x&X3&HD_>~qHE`XG!M#k7|iA(bNvzm5^p|*k@ zmB&d7+I*V5Ei^8babarhTGMQc@1q_rg~GTAv~>)$iUj@T!Gi~LbKbZ{!d3^)srHT! z`;yc-XfTWV16-?#G#P3LwhbgKmboOoJR`%w(NTIzCh;QAxGKVERCOUe0YU;zMPguiPRoBxv zhazPO4<%5SZrys~)FMaazVTogzd{@?b>uE2o)D9v+X(3&LydaZlWA!5u!6BIMw7(5Q~Chd+WI8YxeXMKLR8Tw-@8++5oG7l@TbN1Ra8U0^yLdD&8*7Y}vA9SbvIyU)xg$ zE}SFD-K0bo@ZtORy>XfW&pkUe1)8S4y&WJYqGg;vtO{x6+C6U{pAb$FHp|fhruRRh}LmSp5fCS>2-^9_?mL?d?DViRxkor?odmLnZ?YQh9$*6yK&} zjFzKZAWSLCjs-Lq3v_wL8lLp_Ru`S_cWY44KsMIiQ3uuwXt z;^)twzZw>n`tigw^mH#QwuN02rn52Ly?ecs6mw1=;;*sC1>69@fwwyTNeruVuC|A9*w0tQhf zz>pG1E`6~faR(4K(dNn9CVylpKOGz#j7_7g`l8K8%Kp&ow=g%2Z~k!E9#dE9(!+-D zY6UT$aByfIy8k+i;J!|sIwi34*|l1GskLhjiMsb=_49KboQcVL_H>;aRa87G1DQ~f z1%-s>=jPp<%dnD-c9r(wjS~t{cfUTmSm&FZ>-Fjrps%m6MYb z6NAnq5LpiCx~t0$OTfUqoI6w(g)xLvD;M;{{rhXLr)vaNvJMuXGW*91z{Qn>)IhFz z>a#H2D==?cM*oB~@~X#$5FM8qbI2nA<;ZoE3}dkr6aA3gkxsulD}xY(0`1wi05O7t zOBWYrDH4rGjvc#J)$C#g@*;M0boA(RXJoBF&>)?ioe-jNJOWBfOAVzUFrkAx4~b%% z)2B|?r-B+9M->s+jecL>89n^v4mZKKfkHo?6){VO1q})8<3eFI8aGLWKqk)0+SPtYP?G(z} zw`_SUy)S&Qfcya;aSKi&M$=cntjrT9ak zP(FP+g3FC-+?WRVNL2IWDY_>a$>bjZ{e+o?1%Ym|F&dTJ!^i(5xRe2^Dp%19y{cRmx`Bq*W7Ak^i%=Z#9_Wmhp%iH_&@&0K2iXhk^1}_nE=( zskr~TQ(o>zaPh2xP>h=UmsvNijj05F!vy zC?n40p+mU!%Pr%J04{wR03AG`x+vL{RU#p>q-0t2{)unkP#bm9N)W+z&)JRS}KGG0DRczk8 zBDK$WKW|dw;-9v2IIy{&ouQV(B7i-EkOQ#*g!5*s1IQtewYC?;i+yJQAR%g*k`}+gLVpcPM;qtq-aD)%Xl?a@#LvfnTIw8XOsLZJB(UJe2uT`RKh!gfOIluSOz6VnILu!s0AJ+pz}LMy;=|s3l@t{L zOWmKz_$}}sB~@5hT5jF41soP4-npa9Qks{5U3q4ZtU$lxL10VqP_uG%-G&f#cc0`C zQtJHut^D!hGvM|?Oj0BwaVv_@jRQ1o?Clwm1mQICRu`ik$>yg#Vb_8un-UhIBzBf~i9N;V1Hcx(Uz3)1$Ws7iRtxWkq>q@pM(BUA_zQ`2f%jY0M> zHZuCy(gMx)5Ts8KWXUp}-!J5Tnua)x^V>C1F$!4p`nB%$yEp6Y@_2Z7()T5@frowm z+^y$rG!_#NLqMwN=o5~PtRnX^aUwrvnY#BJT=bLJO(MZRAkGsy62E|>wP)|%myR=A zC>Z1sz)5SBwgKi&{2?!E#xNAm|y5-#$G^`1l!#d@Mv$^wTK9fOMhg^tT!SX zNvh#qGv>#R5x3$!rGP5$pdfJb=1rQMsU;p$w_Ug)TE-N&1VSY*59f+L&hWYWuoacf z3#%aV8p%?c_t?>+h$$7RMgHk}K95ApzW8FN%oz;DlDR~oe z?}1ESG`5ic01o~}7oftAJTfENm=qHse)AgLJv2o*r2huPCp!EiywQ}Xf&I&r*1C~hT?wj$+$?5?`DcL8aFGdAt z+qZ1la%K5ydIUT;in6jYU~hs*cK`L2R^0rs+7=dH!HM88b3YG)MWdOx#7&aktZf!s zc9b;qiiRmJzP`Ew&xWK68XTw-cl7S=PUx-2cC#e-6@Z4?zI}C$IfYhRwD%o%V``mS z(-a5s12(KBGUl)7XwOTRAlBmK0FrD(;ICWP1GI=60(GUgW`$L=Saox(VnF>c&zU^4 zN63UY+suFfCr+%3sLonSvT#vczRYMLsYWWK(8P+vJXhU z<)vNTfrAG-yvK;{7nu}zEjl_HPXSF8va&+_09*ha%yg4!q#vTt z-o0-xFI$O6{95kUf#w2XR}N1S#N6t$Gv~gQtfr;jsODHpAlIC$H0s4TUU3=j)X0d# zt+}JUJ+ZO(@A?;3^>_WVv}`AyNOY&*(mkHKD(i1rX}_hrs|(4N8D}FqJG`z3GB&{g z78ir0F1mYHbSZzFYL+5`wK*199gU~S;ln*cLpKm7UTfG%ab7ds$j&n}UYsF-M4Rxq z&{y@1jF3^!!F_=5ppX6#qi~&ur~+mbe?l%x)=_16LZVQq)Yq?H102E23C15J0)7#Q z2`wPpceqsB^E1$%Z~>ad<@f8*m00aVrl!Z;1)~~j=rt>B6xOU+1M4Ew616ft-TvYN z_^(PJ#3xUlz#xXtqw?*Plw`od&1iml^@^a!VEzLvM6A~1fNucB15SDJ+a)eMyoFq^ zOQFZk^It!IKKc(&)>?Sc0q7X@_dkpkOd&S7iG><^3Ve#JaH4@1#@!cMG<1(H0fvg& z+C8#-sM0{~TF~u|9TVIYai?ljF?~iywq)ONI}qYG{x3NcUtM!kKu8Fuz7@p6fdf3d znv;NvtV#uon`0Li7HAY!-{l&5kL{un1#n^f4?Jj00>tQ~QlD<47DNU-|OPnZs-`+37#Jwi~H!YH{c8M9gB+!J-TADiXTFJ0b&^%9K3%ZGdg+|?h?!c+IYd7zF|UW88`L28r(gO5(}9xPU*~-snKrhx##KG z*}stN%jbUAfkGlF)4^QL+5ha+Kx%|Kv1V++2+1`X8bHhe=%n`T3kOk$i^GEfOhMot zWOBeU@WVtvg_sd$Lr$$%c6r}CdlSm#eaPP49;B<)^m_P=mA7v%>|T5o=d83Z#?q;ZbE}2jO(ZZsUZaMnlnYvU+vW*%`i4LwKiv(Zbavb@?hf z)OO+!N^Xu{fQuIr0z3zZQUX&&)tYcc*+LSKla_{}jZF40=Fa%I=o0XYi#rM(!2De4 zM>(CKN}J_^ijH4jTtGK^2_)yhqNY(~o#x^=Y-Tpf^JwY+{Ay=MEAsWL#hRNX4L4n? zv7Btnmk;Jz*SL*-4-sQ#V$!0Kis+s^e6FI;h>nWt?(hFnrtzt&ipW}MdWL3?CjnJm zyY~QQn@5(#M^JAxIHLDHgA*J;9%mEKYE@X(68X3Utww&-u0)KF^)_-XTLvQ(QL3{o zFHMcu;@={Na&mHhnfeYlr^uhj9xiNGe~*k{*(#_vpKgpk{pt#b#@q-4(g?`De_W-E z|DqEEGBSFJ)D=%>;J3GQ^h(_+z?3jOc83|1@%Frlc#zcA!& zDtS}?$KW6sjpY0HFIRW(^qnJ=c5E$>W8RL)`noz~c^jyiH}zFJHQ}dt{`j#`+a$U2 zXU8?A>jIkt8+*GaC|UuUgd-PG;H+dW&0F;11)%K<5H%sMJf}t^5SqlI9eoNiDJeGg z1&#J>s5Pir?ab$e0U+!;56{hkgT_vBtD^4pB7~Nl6rZ5{`}j+AmK|u;X<6zi=CW zPM^k^FZnNCs7OUELlZ*d6mKOYV6&6KMB(C+#>XG#Z2s79KyG3}5{!+GhDjB7aqZj=f+7Y0fR__wjL}xd2`i7avz9 z1TL7=2~^2tPcVtA6wZEC8LH39en$GhFWKeF@B|k+VQ&xiobvIbk+HEK935DRuf^`? z0GglzW1l;J{X$O|-ooU3Ad}pjZiKr7UPSH8_u~1P7HjwuM=mW*>{~D&`~@^JLW&U{ zo_Fuw;jxn!bVQZU3S+PP`%iO*96Wk-86YCsHNuHyS@9g$9YX}dSaURED4>Jmrb0X1 z9^LkJ;Xg)_kdXK?d-1~+kP8x5?hUJOC@OJyr|-B>n?o z4~1}w3!*zoYRss8EuilG0@pJ=O2T8X^T|Rb$weT*ISzI$sut$^iIEX>w-5~(U`VvG z0%1|{@#CuEx)@kelT9<%YTjG$n z5C(r=O-w9l3G(TLaMr(li*6T~mcUKwNnAh$JLlK$yTb~C6ea1?n~0$~_6k@wHf&gy zDzlC#rJ(o2!yDiL0Jp%w!C@gsgf>(wlvrRED9rOmu3TYxa%|@Pf%VoToH23klAlin zRo`REBlX@r1)OPM&_jnl>=-T~C~}yVfgs=95JIfnNlZ-p$6^LYB~BgS*v0lDSKzb| zNLQPDel*ANlcH)3_wD=DRv=HlnhuZflauv=X$Oc?#T*2^V-$uUlq#B2A?N;mLQ#Qz zK$mEhHRR>x@gzBYVle)m`JzaTbZCB`@Uo$Vq&`1>aK`)k*Z~+=Ela(&?HZn6y?mFW zBNerUkiT}$=fFRN!wNA$#La!`&y#Z<>Vx}^@7le)d0O%0@#CpVzRnqzBuE)nM1c%W zhldLQidS#?{|GiU=bgDh1tYiO1Y8RXdo`e`g!TsW78D8jduhX&He*fFJW2_PckW!T zV<~51VroTe2kRE-E&wSAY6t!N7Ll!y%wbz-tgX$<&nNgTBEDG(1Tmy z@Zo4BUzc7r`m0y3R`mFC{f7s3A5Zh!L}-OU zK|!F}k+Ed0%Y;)67;#{;OU1r<^7&_F$?%!3vnqDGQP+d65MAKay3lRgK)HyBBqk*h zUMG05@Q|*>zu^xfl=N-e2qNuAlrcbLvWz0^ISDZ_8Kp@bdx*@&#>Lgrlrrnr0B4hj z+BZyO;b%)`e-*>(y_}f=ee>Ql%n1BxvGxl_aRjdC2^AI=#%l8N^RqBAs?j9J0NBoR za^z9CQcNCvod2>%ox$vh{jz!Eg}-j!b}9UNNp9$C4)Z;>8P^CMT|-q{J6i zg^@EHU%-dK-rkUw5HsXPTYY_2(Dv|!47C@VX31+!7QBn6Vq9dtk=kD7lkn<0>~tua zmB<4tSFhf)*!{2^6GZEzq)yMer~AM;pJ!&cSRlVRfW|>hlq;t>79m&dG^61==K-4U zOIaebQhA#^_lgxMIXPUGqifJD3v*^fYd}ZE%ta0kbrE$wECGE1zRC7V_H=ZURWeI8z4>AFk?g z$;!&Y&3JftAgsZOL*etC-GqH$UA z=GfM`LbZdMn*O&%qd&_%%*x8U)Hwyfxw$Cf?lH`cEO|);1O!+a1NW!yTDqQjq00#{ zTX-tWyFn9Ch%=;e8)Y7Ph@KpKm-5PLMK@C?20+|@Sx~R_;;P-}RXmIqQ5e(i-^ab8 zh9jgnXku6r`l@a7m>(MG?ha}hPug-~FCpbMYi!!|STaK+c!GXpVj^uQLlt8S7z2O^ zh6h21hsj7BS3Evi9HRs!qOa9B|COeH5`0AU4WXFW*sCq(t5Zx~AdEPD_gnB+B%CYT zjzJUZHHU*5Zv6-iT#F!3Z{G%ePNZ3=pn|8V$Gbo$>DHNbdB2=Jngz*!< z2vcM3EO#AE-W18{lfS8-%xRxzK|+-kP&qG7uG;$GoW}I*a5>U5+O+~ z4-9Pohwhj-WmxjuZ{`<57N zf$OM5N1vWVETT&PLkegq(D-`)s;K0JC;zk~ZS9({F&)PZP_OyP$jes`oO*y{W0x0u z#qX9pp+|z)I@2MPC4xy5P<+tU@lz3E8b1-QmK8s{x^5*V*3%kk@5y#|a_j=H)oyep zvovGMlyaeksIc($ruO|yrgBKih{wT>az8X3SlcD2eWAn~D)3QSaYVILc5c;-VmNw- zuL2uv{HnYU1~JSFuor0`1U`bvzhSa*DhJF#^_w?@ti;U-K)=-D{cVK704V~4n2FP0 z`*J|%QAvr~vBWp8PtxU@$qJUuH`dqhJl{z|69EDk(jOQ`BCG z-2pI57*s$eFJG?ax1ffz4AGn|SQ`utNYco&8JqKtlAvk#6T^yC8lVzw$=iy+<>EJ; zW`L!EjX!!A`GG#EU7ti)RgC>g%1Z3cor7?+Nmdp=DGYZgQZ0E$WYuvx1%c}j{d7#{ zbU;4q>N3i4>0;t61T%pDku@VDBbQ{&|M6FT`BDUq>c3>$rA7tQC)h8+vgpd=bl}*^ z&{$DcEyBHa^uPgn%aj%PkvPL$jq=87#x&~vJdH*VTxW2bveiMljLbLYMtiE_5-U*~-ca91;2;G+kIAxR20H@`S#|RJbZ59zqM0{mzcrEZbZX_0Jz` zS-#^J+$|7LnTPfk9CVOBa_A8DIHYd9%xTpA`_mWqEb7rlL3(<75iTZTHRy(ksmcYG zCuD09y5sz!kS=szxhm7m=+=?1@N`g*KsKQ{N4Z85knf|=Af*xs~buu;&4ytrfI<1s5z!fB7;-ud}?*sxP{ z6txEuDmerdYlxY!E@Q%L=>F>yV?sS2Td{_rf#L+B z=wH6kztv&%xzJ%O(lzwPB>f5R-1<4AH8&}9vDq+su3o)b=gM+WUgsf>;RG&qMF-Oj zLvP#wVYoE^M4RRDn=oHJQ?l5syJt^mQc{wI99CYlg9&rcyOXLNzhbtCFeI ItU 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 G=>GtXw!dNk literal 0 HcmV?d00001 diff --git a/ui/public/apple-touch-icon.png b/ui/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0104f97e5cf414f2a0dc6816c83eceffabb16999 GIT binary patch literal 8062 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 literal 0 HcmV?d00001 diff --git a/ui/public/favicon-16x16.png b/ui/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..a8884bff578f90b057a422de2bf0a088e09c5cc6 GIT binary patch literal 669 zcmV;O0%HA%P)+9>#^@o{&==3sQILYFw=W3H zHW41H>SQ{dUR?eI)22!;oBiIONYvi~4uc%=egCs%d7}WnZ|}7%s|oZ8@N{}QeHKsy zph}Yn9|2EQ+EwMUF~KJh`!#6T^Gu5|#jQag&w-cOZ0a=NucV+#4sc7ak#P|&1cCVg zM2*oF+r|~)1@I=Dbxs1_niQ(5YnOnYXtd_7YH>is#DK{lD0UcQ993Qc@2aXQkI&9d z6jmppKr?lalhVX)gGF+QK3bI*Cs|Ns2Yd7gVO{EvtHw-OBv4fWM(@rOiYt%$Bx zl|aNg7z8H|0?BJiz;WDLLB0&E?%+Nwl6)cvf~Q{#0^&H{H$ZPaP24GwWHbnZr`jFX zt3fXm+?_4pr$Dybws>LabXk^O1UzbdD^;yW`})p%nL?Sf1$Ug{{UDz+X4SIvsvs!; zV0wCb27+W}W+tpwXZG~=t{wy~240uVHmcR?%n}vON`XSb-2vQg%sG&2eBb{KKt7+( zIgWQVU;{K7we29&0IIU7J=Yl%aGc`3skN`MkFsxYw9< zkZXP4Kadi0hAk^O1bD{(*ucP=`v3+823Co1N%}Z>zCEwK9B{`e-VVCMmn7ELs(!esTKnjD5fuu>8$fm$@C3Li z49njGa9wvm)p{J{9VYzual8ng&wHN%nyM{`$mTFC?}fBvJw2-)>Fr&0X|+1@%Z>^- zj(0KWlR!71s&aD_l^!w)`WF^-OoR(f_@8RE?kk$oI*xk_$Tv;i82ES?miItfGTV-y z04_*XOWqa)!OdK5Z!3zcN<`(dVs%&Y3 z=W@BcZO3CxaL^}%Aov@!HDNZ}v!!{QRQ16q@*e=`>pO4t{CsUcur_7SvzEn`yC7~Hrv%zdyMpj z0`Qz_*{Eu|!#T#0hHBr@1hGF?(fMT)udXdaQpxG~ftkt@&ZuZL(0=Zl+54^>g zdrq7<@vjsh{DKIVnde87WMM-XhLdLJA5-PKCXb4+EsFeIkbWUG8XQpN-DZ1FuUL^C zZ9j@FC9uz5RDx%lb5*W4(}_5?HdHDTQ7UcmI^bc_lcb=x1%dw^NK00)*Y*P!HS=XM zT-n@IM*pQOe!sb1gwF?o|1C)Ge%r470(g%}@SKQl zD3!`BIXfyKFM>7|7+NHwhefzdMAsJz#XD6IRlO6)nut4sz~2clI5^mw$#m@l-fNa0 zRgKsCepKlM4lO;9B()bvuUk~PyoJC$q|7p^+(-IG>@sm*3Pb;E0IuuyWHMbp2i|9v zJ|D;S`Y4JfUI}~&fyv3qDzML(>vB2oT>ujk{z2eIRa)ye1#F3;@>c;o&s(V~dx3Sv zf6TV!@=E2!;6H&w+fA5$N@eBYmgRNibhdm%uh(Re0`qFI2Ek&*6db#5=n zs7d*PWhLu;-yc66_{#_Y;yA^h0viET`E52Un@goqxzl3L^NNjz{z#Qe&7*N>ygv-X z6UzbbM1bMp;WhK~3x5Wh$8rkT1=bI3TaR0orS-awK(bktn?zV+;!Y-s^?`|rlgF0} z-jM*LU$!FlZeaZ~(>xW&na#_odHd-2I-Z@Kovl`9cK7zKngw|?oqxFi#zDUlhUHJq z&Al{zrr@x=U$czkc%#5OL5EebMD#dVhr_UR0CRNzZ^yp@Ur~c&t>?DP00000NkvXX Hu0mjfYX=1; literal 0 HcmV?d00001 diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0b205ec2c057c83d4220b1235bba9405a53e1f12 GIT binary patch literal 15086 zcmeI2dyHL09mmgdu`5tnsJnab-M3g2gO3FxR177o0;FdP82mng=mjGB4AJSrJbYbfGCQ3dTQlssQ)Q-y}hoycN87k5=Hw!0}75> zJs^sp8}HZN-u}k+V)5W4kspuV@bl--Z-i%Md#O|<#V&23?)&@hyYC#M8w)~zO-HHp zua0v0iya*u^OV}li7Qt3V9E2JCW9YwDTWl;Y!((hCce6gcgJPW>N^7oV0 zbv8G*p<{t?q#Gd1^aSlZ7yRE;-&QXF0GgN8red+!3j7P@M|xUXO33UZ{}Sm}&=yF^ zuUsy7Q&)v{ITZ!rMJIakk$*&ToILsJ<+ft+YG@m35~7ImNz{*E%M$8#CVvCz#rD?L z_t6*aJ!uwU~N7_oI)8!}fH&xS>C8q5OPTvA7rTY|_&qPgsWL z0@60xbUf|fMPsB?D)qQFJ`>XCmFdfGBV!o-XR6KE{{hMyY2&4N^m%p8A6wzGIkl>u z|MFyXH#e8@=V6`2;@d-fC61zjg{1B%>TimoiX@nFj zt*phF9%v3EDLy4X1)kU5uYj))cmDn{{e3y(=j)U|N>gae^1~)+S zH7v-)jp*zK83B(t-X)H}wulSjld!To{r&}?4g<^$be^*!6o-XdLpfCRE z(LLGO+WO{c+R%VJ#im;OipAR%}ImGb}1%6q zg4P;9{T(T;RC<8%&}_V8LHzvwl*f}t-NUZ_S?yPhpSoj7uR5I8_A?B(*PeSeQGb`i z#rW+$#=|_r=RvnWv;Mu(Xq?Zj{vpvBj=jle&|zandz4cRr#Jy_t=Yi5xLdKs@Ocng zYwISXP4+9JV?cI1z#L~MUHhT)cv>;=NP8F?+CV-XKeCnb11T?KW2}9`ym|9>hb~=j=+o292Khri^yI9Sj}5PV z891A#+S`2AaISA3GG64a0hhaGq2mDP?5r??oRdwC#t`%5UXug7%lD`3rOs^7*gV#F z6$>aoN=n8Od#<+`E`(v9!gHe0Xf9D6)1TOTmgxY#qPc3kiiyjCV2+9bZO{`BU@kIOBKze8khU8sDVr75(%L z4_EChmp^E{lY#j2Oi#|M_~Dbr@BF#W!{18XE{4bc^F7=~f2;)RC8_FGghJFIT15IH)3nGKs6X9t_Cj+U0a!9DHp z7o_!0KlLwK-E^QkznKj>lXFt4{oGkvXM-fZX-{DO2i|RT(}lEUg=?qmNY3tTV<V66SQ@Z2Uy%Bx1tMaM?cP(l39$}P!EBK>kqYWA}tl`Q2 zG_e1Aa9!ispuI1){yN}Q`_=X%#^awRzJQK!yl>N)WwLVJna$4d)_*!TX$_x|AGV)B zGF=IritT0`vKEoTHf*MU4l$Y?2z6H*#>neJ`R;yH`!l^4J>$|U)`ipUjO6n$cTa)F z&b;QEGLYDap=o;`Q+U(q@79`d8-5Y>D`9pxjnsaXT5yx zT{H6idjsQ7-aV&cv(JNlfA{Stw!g{8gInd}>7U=o&u&cJ;Nx6AbGK@;=m-Da!P=0# zyBf_@AUe1BHgLa~dB+E*xoZ02V2tP;`g$Ml@|n9?vkRPmZ(%Z@XAZv0XeJAhS@re% zcRrE@uAcpo&dBN$Uq+JGeX)19Yj%P2?>(#y&*i<{8c#hK+Q%7{m)_AIc*RT8BdFM# z*? zO*ZxZy^FQsnaS>3iJ#x~b+SfYZ8`+̱AWX|-=ja`tXGmhC~F!!!MxVJIdrzZ1$ z%39SI=&Oe7m&(vQ5zxi7Rc{-aGZ(bGLbp1kr97xRH2K8)Y3;u(hjudWuae>49m@~b zdODJ9&fTi{>oN#3ZOD25Guq7ExYN-(WzH8O&+D2SV-#Uy^^9$NcOm%u!7>z;q zUUrwmTOY*uCUZCM=nL#qt_o=>KOC+}Cf|pQ+kK$b_gkqR^23brPy2MrE6#!27|_Ow ziy>|Je(CX~cl#cX&LP)$G>I(}-FLe;_VE1pD)KThRr<8vQ2$)0i*kD3Zgy!M<6LuJ zNUL>E>z~m%7_RBs5088PGa1M{8a=Yv?}t~A@o}RusC6H@=L1^too6%#ng8MYsmZ?{ zn&gh(XjNwZo}*ZftPgqh6UxGK3@L1b&f?p>-{IR{X#YVndulM}jY)43jQt0sUne!{^YWw?f~pD)rmliFn^n`n1ZQ9m)^iZ&UfTH9fN)hP;z4Bl|>Bc#hR~ zlzP|6{B*|%Z8-lGYxG@K=BJnl?RrwCy?OIL!#ITk`}UL36P^Kc&#}xAt>rHTc8T}2 zP_^bH^-qMfdGjpfoore9Nnsl{$=*;FeRCkRy4&@AtM5fm32Aj!g6_!-ZMpndPD~0} zCtaS*IhL_>Fg9(@*i{>ci$cZtK<{grGYI9(88+mZYMB_W?{|H_DxZ6f)qPctU77C} z%ntf2`*v5G%ZA>ea5Cpu>_37Ou7_&F(Y{g0d@ppPeqjvx_#K@`*M!fptQki^pS|0y zeM_F2<@S1`_7ekA-$(jx zo$c}C>&BjGpJQ3$E+Nnz%DXWWu3Is4J^ilBlRaBmy%k@c z(}r|x*Tb=nT{K&U*3q+*?5JmF9?sdmL-)^hY#bM*ae$rM?U8eAUk7daZRNOvXOTej z5POePJY?_m82L>}BA?WO`wz|4S@gpHmMVQK#D_|JspMcqS;_-^xFp!G?}~sC-x%>X z5=D)I!-e63V6`c3NMLu|FIed-@BtEVLBr5O!6i|py9Spw)nH#^0vB}^61b$PA%P1T z`xDrK0P*dP0Lf{LhPr`$h5jXcVP5Sj3{ZvMhM^8_yoVe6E4u?n3Ij_UfpNo7Ux8bv zxWCdRx$(f#CK(?O^(FG-3Idi=OjlzEelbP~0 literal 0 HcmV?d00001 diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg new file mode 100644 index 00000000..c3b25eff --- /dev/null +++ b/ui/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/ui/public/site.webmanifest b/ui/public/site.webmanifest new file mode 100644 index 00000000..8861bd64 --- /dev/null +++ b/ui/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Paperclip", + "short_name": "Paperclip", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#18181b", + "background_color": "#18181b", + "display": "standalone" +} diff --git a/ui/src/App.tsx b/ui/src/App.tsx index f0876ccc..01b26557 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -27,6 +27,7 @@ export function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 0f94586f..ee941215 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -37,6 +37,8 @@ export interface CreateConfigValues { dangerouslyBypassSandbox: boolean; command: string; args: string; + extraArgs: string; + envVars: string; url: string; bootstrapPrompt: string; maxTurnsPerRun: number; @@ -54,6 +56,8 @@ export const defaultCreateValues: CreateConfigValues = { dangerouslyBypassSandbox: false, command: "", args: "", + extraArgs: "", + envVars: "", url: "", bootstrapPrompt: "", maxTurnsPerRun: 80, @@ -65,6 +69,10 @@ export const defaultCreateValues: CreateConfigValues = { type AgentConfigFormProps = { adapterModels?: AdapterModel[]; + onDirtyChange?: (dirty: boolean) => void; + onSaveActionChange?: (save: (() => void) | null) => void; + onCancelActionChange?: (cancel: (() => void) | null) => void; + hideInlineSave?: boolean; } & ( | { mode: "create"; @@ -110,6 +118,51 @@ function isOverlayDirty(o: Overlay): boolean { const inputClass = "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; +function parseCommaArgs(value: string): string[] { + return value + .split(",") + .map((item) => item.trim()) + .filter(Boolean); +} + +function formatArgList(value: unknown): string { + if (Array.isArray(value)) { + return value + .filter((item): item is string => typeof item === "string") + .join(", "); + } + return typeof value === "string" ? value : ""; +} + +function parseEnvVars(text: string): Record { + const env: Record = {}; + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eq = trimmed.indexOf("="); + if (eq <= 0) continue; + const key = trimmed.slice(0, eq).trim(); + const value = trimmed.slice(eq + 1); + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; + env[key] = value; + } + return env; +} + +function formatEnvVars(value: unknown): string { + if (typeof value !== "object" || value === null || Array.isArray(value)) return ""; + return Object.entries(value as Record) + .filter(([, v]) => typeof v === "string") + .map(([k, v]) => `${k}=${String(v)}`) + .join("\n"); +} + +function extractPickedDirectoryPath(handle: unknown): string | null { + if (typeof handle !== "object" || handle === null) return null; + const maybePath = (handle as { path?: unknown }).path; + return typeof maybePath === "string" && maybePath.length > 0 ? maybePath : null; +} + /* ---- Form ---- */ export function AgentConfigForm(props: AgentConfigFormProps) { @@ -175,6 +228,20 @@ export function AgentConfigForm(props: AgentConfigFormProps) { props.onSave(patch); } + useEffect(() => { + if (!isCreate) { + props.onDirtyChange?.(isDirty); + props.onSaveActionChange?.(() => handleSave()); + props.onCancelActionChange?.(() => setOverlay({ ...emptyOverlay })); + return () => { + props.onSaveActionChange?.(null); + props.onCancelActionChange?.(null); + props.onDirtyChange?.(false); + }; + } + return; + }, [isCreate, isDirty, props.onDirtyChange, props.onSaveActionChange, props.onCancelActionChange, overlay]); // eslint-disable-line react-hooks/exhaustive-deps + // ---- Resolve values ---- const config = !isCreate ? ((props.agent.adapterConfig ?? {}) as Record) : {}; const runtimeConfig = !isCreate ? ((props.agent.runtimeConfig ?? {}) as Record) : {}; @@ -195,6 +262,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { // Section toggle state — advanced always starts collapsed const [adapterAdvancedOpen, setAdapterAdvancedOpen] = useState(false); const [heartbeatOpen, setHeartbeatOpen] = useState(!isCreate); + const [cwdPickerNotice, setCwdPickerNotice] = useState(null); // Popover states const [modelOpen, setModelOpen] = useState(false); @@ -213,7 +281,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { return (
{/* ---- Floating Save button (edit mode, when dirty) ---- */} - {isDirty && ( + {isDirty && !props.hideInlineSave && (
Unsaved changes @@ -237,6 +305,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { mark("identity", "name", v)} + immediate className={inputClass} placeholder="Agent name" /> @@ -245,6 +314,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { mark("identity", "title", v || null)} + immediate className={inputClass} placeholder="e.g. VP of Engineering" /> @@ -253,6 +323,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { mark("identity", "capabilities", v || null)} + immediate placeholder="Describe what this agent can do..." minRows={2} /> @@ -303,7 +374,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { ? set!({ cwd: v }) : mark("adapterConfig", "cwd", v || undefined) } - immediate={isCreate} + immediate className="w-full bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40" placeholder="/path/to/project" /> @@ -312,10 +383,24 @@ export function AgentConfigForm(props: AgentConfigFormProps) { className="inline-flex items-center rounded-md border border-border px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent/50 transition-colors shrink-0" onClick={async () => { try { + setCwdPickerNotice(null); // @ts-expect-error -- showDirectoryPicker is not in all TS lib defs yet const handle = await window.showDirectoryPicker({ mode: "read" }); - if (isCreate) set!({ cwd: handle.name }); - else mark("adapterConfig", "cwd", handle.name); + const absolutePath = extractPickedDirectoryPath(handle); + if (absolutePath) { + if (isCreate) set!({ cwd: absolutePath }); + else mark("adapterConfig", "cwd", absolutePath); + return; + } + const selectedName = + typeof handle === "object" && + handle !== null && + typeof (handle as { name?: unknown }).name === "string" + ? String((handle as { name: string }).name) + : "selected folder"; + setCwdPickerNotice( + `Directory picker only exposed "${selectedName}". Paste the absolute path manually.`, + ); } catch { // user cancelled or API unsupported } @@ -324,6 +409,9 @@ export function AgentConfigForm(props: AgentConfigFormProps) { Choose
+ {cwdPickerNotice && ( +

{cwdPickerNotice}

+ )} )} @@ -347,6 +435,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { onCommit={(v) => mark("adapterConfig", "promptTemplate", v || undefined) } + immediate placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..." minRows={4} /> @@ -429,7 +518,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { ? set!({ command: v }) : mark("adapterConfig", "command", v || undefined) } - immediate={isCreate} + immediate className={inputClass} placeholder="e.g. node, python" /> @@ -439,7 +528,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { value={ isCreate ? val!.args - : eff("adapterConfig", "args", String(config.args ?? "")) + : eff("adapterConfig", "args", formatArgList(config.args)) } onCommit={(v) => isCreate @@ -447,15 +536,10 @@ export function AgentConfigForm(props: AgentConfigFormProps) { : mark( "adapterConfig", "args", - v - ? v - .split(",") - .map((a) => a.trim()) - .filter(Boolean) - : undefined, + v ? parseCommaArgs(v) : undefined, ) } - immediate={isCreate} + immediate className={inputClass} placeholder="e.g. script.js, --flag" /> @@ -477,7 +561,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { ? set!({ url: v }) : mark("adapterConfig", "url", v || undefined) } - immediate={isCreate} + immediate className={inputClass} placeholder="https://..." /> @@ -492,6 +576,24 @@ export function AgentConfigForm(props: AgentConfigFormProps) { onToggle={() => setAdapterAdvancedOpen(!adapterAdvancedOpen)} >
+ + + isCreate + ? set!({ command: v }) + : mark("adapterConfig", "command", v || undefined) + } + immediate + className={inputClass} + placeholder={adapterType === "codex_local" ? "codex" : "claude"} + /> + + mark("adapterConfig", "bootstrapPromptTemplate", v || undefined) } + immediate placeholder="Optional initial setup prompt for the first run" minRows={2} /> @@ -543,12 +646,57 @@ export function AgentConfigForm(props: AgentConfigFormProps) { Number(config.maxTurnsPerRun ?? 80), )} onCommit={(v) => mark("adapterConfig", "maxTurnsPerRun", v || 80)} + immediate className={inputClass} /> )} )} + + + isCreate + ? set!({ extraArgs: v }) + : mark("adapterConfig", "extraArgs", v ? parseCommaArgs(v) : undefined) + } + immediate + className={inputClass} + placeholder="e.g. --verbose, --foo=bar" + /> + + + + {isCreate ? ( + set!({ envVars: v })} + minRows={3} + /> + ) : ( + { + const parsed = parseEnvVars(v); + mark( + "adapterConfig", + "env", + Object.keys(parsed).length > 0 ? parsed : undefined, + ); + }} + immediate + placeholder={"ANTHROPIC_API_KEY=...\nPAPERCLIP_API_URL=http://localhost:3100"} + minRows={3} + /> + )} + + {/* Edit-only: timeout + grace period */} {!isCreate && ( <> @@ -560,6 +708,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { Number(config.timeoutSec ?? 0), )} onCommit={(v) => mark("adapterConfig", "timeoutSec", v)} + immediate className={inputClass} /> @@ -571,6 +720,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { Number(config.graceSec ?? 15), )} onCommit={(v) => mark("adapterConfig", "graceSec", v)} + immediate className={inputClass} /> @@ -669,6 +819,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { Number(heartbeat.cooldownSec ?? 10), )} onCommit={(v) => mark("heartbeat", "cooldownSec", v)} + immediate className={inputClass} /> @@ -695,6 +846,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { props.agent.budgetMonthlyCents, )} onCommit={(v) => mark("runtime", "budgetMonthlyCents", v)} + immediate className={inputClass} /> diff --git a/ui/src/components/NewAgentDialog.tsx b/ui/src/components/NewAgentDialog.tsx index 23a23b60..5cce8abc 100644 --- a/ui/src/components/NewAgentDialog.tsx +++ b/ui/src/components/NewAgentDialog.tsx @@ -30,6 +30,28 @@ import { type CreateConfigValues, } from "./AgentConfigForm"; +function parseCommaArgs(value: string): string[] { + return value + .split(",") + .map((item) => item.trim()) + .filter(Boolean); +} + +function parseEnvVars(text: string): Record { + const env: Record = {}; + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eq = trimmed.indexOf("="); + if (eq <= 0) continue; + const key = trimmed.slice(0, eq).trim(); + const valueAtKey = trimmed.slice(eq + 1); + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; + env[key] = valueAtKey; + } + return env; +} + export function NewAgentDialog() { const { newAgentOpen, closeNewAgent } = useDialog(); const { selectedCompanyId, selectedCompany } = useCompany(); @@ -102,16 +124,22 @@ export function NewAgentDialog() { if (v.model) ac.model = v.model; ac.timeoutSec = 0; ac.graceSec = 15; + const env = parseEnvVars(v.envVars); + if (Object.keys(env).length > 0) ac.env = env; if (v.adapterType === "claude_local") { ac.maxTurnsPerRun = v.maxTurnsPerRun; ac.dangerouslySkipPermissions = v.dangerouslySkipPermissions; + if (v.command) ac.command = v.command; + if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs); } else if (v.adapterType === "codex_local") { ac.search = v.search; ac.dangerouslyBypassApprovalsAndSandbox = v.dangerouslyBypassSandbox; + if (v.command) ac.command = v.command; + if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs); } else if (v.adapterType === "process") { if (v.command) ac.command = v.command; - if (v.args) ac.args = v.args.split(",").map((a) => a.trim()).filter(Boolean); + if (v.args) ac.args = parseCommaArgs(v.args); } else if (v.adapterType === "http") { if (v.url) ac.url = v.url; } diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index 4ab5f953..91015774 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -58,6 +58,7 @@ export function OnboardingWizard() { const [command, setCommand] = useState(""); const [args, setArgs] = useState(""); const [url, setUrl] = useState(""); + const [cwdPickerNotice, setCwdPickerNotice] = useState(null); // Step 3 const [taskTitle, setTaskTitle] = useState("Create your CEO HEARTBEAT.md"); @@ -88,6 +89,7 @@ export function OnboardingWizard() { setCommand(""); setArgs(""); setUrl(""); + setCwdPickerNotice(null); setTaskTitle("Create your CEO HEARTBEAT.md"); setTaskDescription("You're the CEO of the company, make sure you have a file agents/ceo/HEARTBEAT.md that tells you your core loop. You MUST use the Paperclip SKILL."); setCreatedCompanyId(null); @@ -406,9 +408,28 @@ export function OnboardingWizard() { className="inline-flex items-center rounded-md border border-border px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent/50 transition-colors shrink-0" onClick={async () => { try { + setCwdPickerNotice(null); // @ts-expect-error -- showDirectoryPicker is not in all TS lib defs yet const handle = await window.showDirectoryPicker({ mode: "read" }); - setCwd(handle.name); + const pickedPath = + typeof handle === "object" && + handle !== null && + typeof (handle as { path?: unknown }).path === "string" + ? String((handle as { path: string }).path) + : ""; + if (pickedPath) { + setCwd(pickedPath); + return; + } + const selectedName = + typeof handle === "object" && + handle !== null && + typeof (handle as { name?: unknown }).name === "string" + ? String((handle as { name: string }).name) + : "selected folder"; + setCwdPickerNotice( + `Directory picker only exposed "${selectedName}". Paste the absolute path manually.`, + ); } catch { // user cancelled or API unsupported } @@ -417,6 +438,9 @@ export function OnboardingWizard() { Choose
+ {cwdPickerNotice && ( +

{cwdPickerNotice}

+ )}
+ - - {agent.status === "active" || agent.status === "running" ? ( - - ) : ( + {agent.status === "paused" ? ( + ) : ( + )} @@ -247,15 +439,43 @@ export function AgentDetail() { {actionError &&

{actionError}

} - - - Overview - Configuration - Runs{heartbeats ? ` (${heartbeats.length})` : ""} - Issues ({assignedIssues.length}) - Costs - API Keys - + +
+ +
+ + +
+
{/* OVERVIEW TAB */} @@ -354,12 +574,18 @@ export function AgentDetail() { {/* CONFIGURATION TAB */} - + {/* RUNS TAB */} - + {/* ISSUES TAB */} @@ -408,7 +634,19 @@ function SummaryRow({ label, children }: { label: string; children: React.ReactN /* ---- Configuration Tab ---- */ -function ConfigurationTab({ agent }: { agent: Agent }) { +function ConfigurationTab({ + agent, + onDirtyChange, + onSaveActionChange, + onCancelActionChange, + onSavingChange, +}: { + agent: Agent; + onDirtyChange: (dirty: boolean) => void; + onSaveActionChange: (save: (() => void) | null) => void; + onCancelActionChange: (cancel: (() => void) | null) => void; + onSavingChange: (saving: boolean) => void; +}) { const queryClient = useQueryClient(); const { data: adapterModels } = useQuery({ @@ -423,6 +661,10 @@ function ConfigurationTab({ agent }: { agent: Agent }) { }, }); + useEffect(() => { + onSavingChange(updateAgent.isPending); + }, [onSavingChange, updateAgent.isPending]); + return (
updateAgent.mutate(patch)} isSaving={updateAgent.isPending} adapterModels={adapterModels} + onDirtyChange={onDirtyChange} + onSaveActionChange={onSaveActionChange} + onCancelActionChange={onCancelActionChange} + hideInlineSave />
); @@ -438,8 +684,8 @@ function ConfigurationTab({ agent }: { agent: Agent }) { /* ---- Runs Tab ---- */ -function RunsTab({ runs, companyId }: { runs: HeartbeatRun[]; companyId: string }) { - const [expandedRunId, setExpandedRunId] = useState(null); +function RunsTab({ runs, companyId, agentId, selectedRunId }: { runs: HeartbeatRun[]; companyId: string; agentId: string; selectedRunId: string | null }) { + const navigate = useNavigate(); if (runs.length === 0) { return

No runs yet.

; @@ -450,61 +696,75 @@ function RunsTab({ runs, companyId }: { runs: HeartbeatRun[]; companyId: string (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); - return ( -
- {sorted.map((run) => { - const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" }; - const StatusIcon = statusInfo.icon; - const isExpanded = expandedRunId === run.id; - const usage = run.usageJson as Record | null; - const totalTokens = usage - ? (Number(usage.input_tokens ?? 0) + Number(usage.output_tokens ?? 0)) - : 0; - const cost = usage ? Number(usage.cost_usd ?? usage.total_cost_usd ?? 0) : 0; - const summary = run.resultJson - ? String((run.resultJson as Record).summary ?? (run.resultJson as Record).result ?? "") - : run.error ?? ""; + // Auto-select latest run when no run is selected + const effectiveRunId = selectedRunId ?? sorted[0]?.id ?? null; + const selectedRun = sorted.find((r) => r.id === effectiveRunId) ?? null; - return ( -
+ return ( +
+ {/* Left: run list */} +
+ {sorted.map((run) => { + const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" }; + const StatusIcon = statusInfo.icon; + const isSelected = run.id === effectiveRunId; + const metrics = runMetrics(run); + const summary = run.resultJson + ? String((run.resultJson as Record).summary ?? (run.resultJson as Record).result ?? "") + : run.error ?? ""; + + return ( + ); + })} +
- {isExpanded && } -
- ); - })} + {/* Right: run detail */} + {selectedRun && ( +
+ +
+ )}
); } @@ -513,7 +773,7 @@ function RunsTab({ runs, companyId }: { runs: HeartbeatRun[]; companyId: string function RunDetail({ run }: { run: HeartbeatRun }) { const queryClient = useQueryClient(); - const usage = run.usageJson as Record | null; + const metrics = runMetrics(run); const cancelRun = useMutation({ mutationFn: () => heartbeatsApi.cancel(run.id), @@ -523,9 +783,9 @@ function RunDetail({ run }: { run: HeartbeatRun }) { }); return ( -
+
{/* Status timeline */} -
+
Status: @@ -551,26 +811,26 @@ function RunDetail({ run }: { run: HeartbeatRun }) {
{/* Token breakdown */} - {usage && ( + {(metrics.input > 0 || metrics.output > 0 || metrics.cached > 0 || metrics.cost > 0) && (
Input: - {formatTokens(Number(usage.input_tokens ?? 0))} + {formatTokens(metrics.input)}
Output: - {formatTokens(Number(usage.output_tokens ?? 0))} + {formatTokens(metrics.output)}
- {Number(usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? 0) > 0 && ( + {metrics.cached > 0 && (
Cached: - {formatTokens(Number(usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? 0))} + {formatTokens(metrics.cached)}
)} - {Number(usage.cost_usd ?? usage.total_cost_usd ?? 0) > 0 && ( + {metrics.cost > 0 && (
Cost: - ${Number(usage.cost_usd ?? usage.total_cost_usd ?? 0).toFixed(4)} + ${metrics.cost.toFixed(4)}
)}
@@ -582,13 +842,25 @@ function RunDetail({ run }: { run: HeartbeatRun }) { {run.sessionIdBefore && (
Session before: - {run.sessionIdBefore.slice(0, 16)}... +
)} {run.sessionIdAfter && (
Session after: - {run.sessionIdAfter.slice(0, 16)}... +
)}
@@ -612,6 +884,22 @@ function RunDetail({ run }: { run: HeartbeatRun }) {
)} + {/* stderr excerpt for failed runs */} + {run.stderrExcerpt && ( +
+ stderr +
{run.stderrExcerpt}
+
+ )} + + {/* stdout excerpt when no log is available */} + {run.stdoutExcerpt && !run.logRef && ( +
+ stdout +
{run.stdoutExcerpt}
+
+ )} + {/* Cancel button for running */} {(run.status === "running" || run.status === "queued") && (
); } /* ---- Log Viewer ---- */ -function LogViewer({ runId, status }: { runId: string; status: string }) { +function LogViewer({ run }: { run: HeartbeatRun }) { const [events, setEvents] = useState([]); + const [logLines, setLogLines] = useState>([]); const [loading, setLoading] = useState(true); + const [logLoading, setLogLoading] = useState(!!run.logRef); + const [logError, setLogError] = useState(null); + const [logOffset, setLogOffset] = useState(0); const logEndRef = useRef(null); - const isLive = status === "running" || status === "queued"; + const pendingLogLineRef = useRef(""); + const isLive = run.status === "running" || run.status === "queued"; + + function appendLogContent(content: string, finalize = false) { + if (!content && !finalize) return; + const combined = `${pendingLogLineRef.current}${content}`; + const split = combined.split("\n"); + pendingLogLineRef.current = split.pop() ?? ""; + if (finalize && pendingLogLineRef.current) { + split.push(pendingLogLineRef.current); + pendingLogLineRef.current = ""; + } + + const parsed: Array<{ ts: string; stream: "stdout" | "stderr" | "system"; chunk: string }> = []; + for (const line of split) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + const raw = JSON.parse(trimmed) as { ts?: unknown; stream?: unknown; chunk?: unknown }; + const stream = + raw.stream === "stderr" || raw.stream === "system" ? raw.stream : "stdout"; + const chunk = typeof raw.chunk === "string" ? raw.chunk : ""; + const ts = typeof raw.ts === "string" ? raw.ts : new Date().toISOString(); + if (!chunk) continue; + parsed.push({ ts, stream, chunk }); + } catch { + // ignore malformed lines + } + } + + if (parsed.length > 0) { + setLogLines((prev) => [...prev, ...parsed]); + } + } // Fetch events const { data: initialEvents } = useQuery({ - queryKey: ["run-events", runId], - queryFn: () => heartbeatsApi.events(runId, 0, 200), + queryKey: ["run-events", run.id], + queryFn: () => heartbeatsApi.events(run.id, 0, 200), }); useEffect(() => { @@ -657,7 +982,56 @@ function LogViewer({ runId, status }: { runId: string; status: string }) { // Auto-scroll useEffect(() => { logEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [events]); + }, [events, logLines]); + + // Fetch persisted shell log + useEffect(() => { + let cancelled = false; + pendingLogLineRef.current = ""; + setLogLines([]); + setLogOffset(0); + setLogError(null); + + if (!run.logRef) { + setLogLoading(false); + return () => { + cancelled = true; + }; + } + + setLogLoading(true); + const firstLimit = + typeof run.logBytes === "number" && run.logBytes > 0 + ? Math.min(Math.max(run.logBytes + 1024, 256_000), 2_000_000) + : 256_000; + + const load = async () => { + try { + let offset = 0; + let first = true; + while (!cancelled) { + const result = await heartbeatsApi.log(run.id, offset, first ? firstLimit : 256_000); + appendLogContent(result.content, result.nextOffset === undefined); + const next = result.nextOffset ?? offset + result.content.length; + setLogOffset(next); + offset = next; + first = false; + if (result.nextOffset === undefined || isLive) break; + } + } catch (err) { + if (!cancelled) { + setLogError(err instanceof Error ? err.message : "Failed to load run log"); + } + } finally { + if (!cancelled) setLogLoading(false); + } + }; + + void load(); + return () => { + cancelled = true; + }; + }, [run.id, run.logRef, run.logBytes, isLive]); // Poll for live updates useEffect(() => { @@ -665,7 +1039,7 @@ function LogViewer({ runId, status }: { runId: string; status: string }) { const interval = setInterval(async () => { const maxSeq = events.length > 0 ? Math.max(...events.map((e) => e.seq)) : 0; try { - const newEvents = await heartbeatsApi.events(runId, maxSeq, 100); + const newEvents = await heartbeatsApi.events(run.id, maxSeq, 100); if (newEvents.length > 0) { setEvents((prev) => [...prev, ...newEvents]); } @@ -674,13 +1048,41 @@ function LogViewer({ runId, status }: { runId: string; status: string }) { } }, 2000); return () => clearInterval(interval); - }, [runId, isLive, events]); + }, [run.id, isLive, events]); - if (loading) { - return

Loading events...

; + // Poll shell log for running runs + useEffect(() => { + if (!isLive || !run.logRef) return; + const interval = setInterval(async () => { + try { + const result = await heartbeatsApi.log(run.id, logOffset, 256_000); + if (result.content) { + appendLogContent(result.content, result.nextOffset === undefined); + } + if (result.nextOffset !== undefined) { + setLogOffset(result.nextOffset); + } else if (result.content.length > 0) { + setLogOffset((prev) => prev + result.content.length); + } + } catch { + // ignore polling errors + } + }, 2000); + return () => clearInterval(interval); + }, [run.id, run.logRef, isLive, logOffset]); + + const adapterInvokePayload = useMemo(() => { + const evt = events.find((e) => e.eventType === "adapter.invoke"); + return asRecord(evt?.payload ?? null); + }, [events]); + + const transcript = useMemo(() => buildTranscript(logLines), [logLines]); + + if (loading && logLoading) { + return

Loading run logs...

; } - if (events.length === 0) { + if (events.length === 0 && logLines.length === 0 && !logError) { return

No log events.

; } @@ -697,9 +1099,62 @@ function LogViewer({ runId, status }: { runId: string; status: string }) { }; return ( -
-
- Events ({events.length}) +
+ {adapterInvokePayload && ( +
+
Invocation
+ {typeof adapterInvokePayload.adapterType === "string" && ( +
Adapter: {adapterInvokePayload.adapterType}
+ )} + {typeof adapterInvokePayload.cwd === "string" && ( +
Working dir: {adapterInvokePayload.cwd}
+ )} + {typeof adapterInvokePayload.command === "string" && ( +
+ Command: + + {[ + adapterInvokePayload.command, + ...(Array.isArray(adapterInvokePayload.commandArgs) + ? adapterInvokePayload.commandArgs.filter((v): v is string => typeof v === "string") + : []), + ].join(" ")} + +
+ )} + {adapterInvokePayload.prompt !== undefined && ( +
+
Prompt
+
+                {typeof adapterInvokePayload.prompt === "string"
+                  ? adapterInvokePayload.prompt
+                  : JSON.stringify(adapterInvokePayload.prompt, null, 2)}
+              
+
+ )} + {adapterInvokePayload.context !== undefined && ( +
+
Context
+
+                {JSON.stringify(adapterInvokePayload.context, null, 2)}
+              
+
+ )} + {adapterInvokePayload.env !== undefined && ( +
+
Environment
+
+                {JSON.stringify(adapterInvokePayload.env, null, 2)}
+              
+
+ )} +
+ )} + +
+ + Transcript ({transcript.length}) + {isLive && ( @@ -711,30 +1166,119 @@ function LogViewer({ runId, status }: { runId: string; status: string }) { )}
- {events.map((evt) => { - const color = evt.color - ?? (evt.level ? levelColors[evt.level] : null) - ?? (evt.stream ? streamColors[evt.stream] : null) - ?? "text-foreground"; + {transcript.length === 0 && !run.logRef && ( +
No persisted transcript for this run.
+ )} + {transcript.map((entry, idx) => { + const time = new Date(entry.ts).toLocaleTimeString("en-US", { hour12: false }); + if (entry.kind === "assistant") { + return ( +
+
+ {time} + assistant + {entry.text} +
+
+ ); + } + if (entry.kind === "tool_call") { + return ( +
+
+ {time} + tool + {entry.name} +
+
+                  {JSON.stringify(entry.input, null, 2)}
+                
+
+ ); + } + + if (entry.kind === "init") { + return ( +
+ {time} + init + Claude initialized (model: {entry.model}{entry.sessionId ? `, session: ${entry.sessionId}` : ""}) +
+ ); + } + + if (entry.kind === "result") { + return ( +
+
+ {time} + result + + tokens in={formatTokens(entry.inputTokens)} out={formatTokens(entry.outputTokens)} cached={formatTokens(entry.cachedTokens)} cost=${entry.costUsd.toFixed(6)} + +
+ {entry.text && ( +
{entry.text}
+ )} +
+ ); + } + + const rawText = entry.text; + const label = + entry.kind === "stderr" ? "stderr" : + entry.kind === "system" ? "system" : + "stdout"; + const color = + entry.kind === "stderr" ? "text-red-300" : + entry.kind === "system" ? "text-blue-300" : + "text-foreground"; return ( -
+
- {new Date(evt.createdAt).toLocaleTimeString("en-US", { hour12: false })} + {time} - {evt.stream && ( - - [{evt.stream}] - - )} - - {evt.message ?? (evt.payload ? JSON.stringify(evt.payload) : "")} + + {label} + + + {rawText}
- ); + ) })} + {logError &&
{logError}
}
+ + {events.length > 0 && ( +
+
Events ({events.length})
+
+ {events.map((evt) => { + const color = evt.color + ?? (evt.level ? levelColors[evt.level] : null) + ?? (evt.stream ? streamColors[evt.stream] : null) + ?? "text-foreground"; + + return ( +
+ + {new Date(evt.createdAt).toLocaleTimeString("en-US", { hour12: false })} + + + {evt.stream ? `[${evt.stream}]` : ""} + + + {evt.message ?? (evt.payload ? JSON.stringify(evt.payload) : "")} + +
+ ); + })} +
+
+ )}
); } diff --git a/ui/src/pages/Issues.tsx b/ui/src/pages/Issues.tsx index ea7b9f3e..2dec2246 100644 --- a/ui/src/pages/Issues.tsx +++ b/ui/src/pages/Issues.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; +import { useEffect } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; @@ -12,8 +12,9 @@ import { StatusIcon } from "../components/StatusIcon"; import { PriorityIcon } from "../components/PriorityIcon"; import { EntityRow } from "../components/EntityRow"; import { EmptyState } from "../components/EmptyState"; +import { PageTabBar } from "../components/PageTabBar"; import { Button } from "@/components/ui/button"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Tabs } from "@/components/ui/tabs"; import { CircleDot, Plus } from "lucide-react"; import { formatDate } from "../lib/utils"; import type { Issue } from "@paperclip/shared"; @@ -26,6 +27,18 @@ function statusLabel(status: string): string { type TabFilter = "all" | "active" | "backlog" | "done"; +const issueTabItems = [ + { value: "all", label: "All Issues" }, + { value: "active", label: "Active" }, + { value: "backlog", label: "Backlog" }, + { value: "done", label: "Done" }, +] as const; + +function parseIssueTab(value: string | null): TabFilter { + if (value === "active" || value === "backlog" || value === "done") return value; + return "all"; +} + function filterIssues(issues: Issue[], tab: TabFilter): Issue[] { switch (tab) { case "active": @@ -45,7 +58,8 @@ export function Issues() { const { setBreadcrumbs } = useBreadcrumbs(); const navigate = useNavigate(); const queryClient = useQueryClient(); - const [tab, setTab] = useState("all"); + const [searchParams, setSearchParams] = useSearchParams(); + const tab = parseIssueTab(searchParams.get("tab")); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), @@ -86,16 +100,18 @@ export function Issues() { .filter((s) => grouped[s]?.length) .map((s) => ({ status: s, items: grouped[s]! })); + const setTab = (nextTab: TabFilter) => { + const next = new URLSearchParams(searchParams); + if (nextTab === "all") next.delete("tab"); + else next.set("tab", nextTab); + setSearchParams(next); + }; + return (
setTab(v as TabFilter)}> - - All Issues - Active - Backlog - Done - +