From 30c042bf118e21aba52a71a4a6d474f06e1ec1d4 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Wed, 8 Jul 2026 12:30:49 +0900 Subject: [PATCH 01/10] v1.1.0 --- package.json | 2 +- packages/extension/manifest.json | 2 +- packages/extension/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5e921f95..cbce39b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "selection-command-monorepo", - "version": "1.0.0", + "version": "1.1.0", "private": true, "description": "Selection Command - Monorepo for Chrome Extension and Hub", "author": "ujiro99", diff --git a/packages/extension/manifest.json b/packages/extension/manifest.json index 6395d9d6..872521ed 100644 --- a/packages/extension/manifest.json +++ b/packages/extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_extName__", "description": "__MSG_extDescription__", - "version": "1.0.0", + "version": "1.1.0", "default_locale": "en", "icons": { "128": "icon128.png" diff --git a/packages/extension/package.json b/packages/extension/package.json index aa544e58..3da12c78 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "@selection-command/extension", - "version": "1.0.0", + "version": "1.1.0", "private": true, "author": "ujiro99", "license": "MIT", From 53f97463a9e79f71f9d90dfa49d59afc2aa16d7a Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 9 Jul 2026 13:47:11 +0900 Subject: [PATCH 02/10] Add: support %pageUrl placeholder in searchUrl templates Allows commands (tab/window/popup/sidePanel/pageAction) to embed the current page URL in their searchUrl via a %pageUrl token, alongside the existing %s selection-text placeholder. Co-Authored-By: Claude Sonnet 5 --- .../extension/src/action/backgroundTab.ts | 1 + packages/extension/src/action/pageAction.ts | 1 + packages/extension/src/action/popup.ts | 1 + packages/extension/src/action/sidePanel.ts | 1 + packages/extension/src/action/tab.ts | 1 + packages/extension/src/action/window.ts | 1 + packages/extension/src/lib/utils.test.ts | 41 +++++++++++++++++++ packages/extension/src/lib/utils.ts | 4 +- packages/extension/src/types/index.ts | 1 + 9 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/extension/src/action/backgroundTab.ts b/packages/extension/src/action/backgroundTab.ts index f06ae7c8..8ddd70df 100644 --- a/packages/extension/src/action/backgroundTab.ts +++ b/packages/extension/src/action/backgroundTab.ts @@ -21,6 +21,7 @@ export const BackgroundTab = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, + pageUrl: location.href, }, active: false, }) diff --git a/packages/extension/src/action/pageAction.ts b/packages/extension/src/action/pageAction.ts index f6651301..dd1788ca 100644 --- a/packages/extension/src/action/pageAction.ts +++ b/packages/extension/src/action/pageAction.ts @@ -46,6 +46,7 @@ export const PageAction = { searchUrl: command.pageActionOption.startUrl, selectionText, useClipboard: needClipboard || (useClipboard ?? false), + pageUrl: location.href, } const openMode = useSecondary diff --git a/packages/extension/src/action/popup.ts b/packages/extension/src/action/popup.ts index 744a0ff7..04062274 100644 --- a/packages/extension/src/action/popup.ts +++ b/packages/extension/src/action/popup.ts @@ -31,6 +31,7 @@ export const Popup = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, + pageUrl: location.href, }, top: Math.floor(windowPosition.top + position.y), left: Math.floor(windowPosition.left + position.x), diff --git a/packages/extension/src/action/sidePanel.ts b/packages/extension/src/action/sidePanel.ts index bd25ec90..3f3cd7e5 100644 --- a/packages/extension/src/action/sidePanel.ts +++ b/packages/extension/src/action/sidePanel.ts @@ -39,6 +39,7 @@ export const SidePanel = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, + pageUrl: location.href, }, clipboardText, ) diff --git a/packages/extension/src/action/tab.ts b/packages/extension/src/action/tab.ts index 5c00341b..94e6eb1d 100644 --- a/packages/extension/src/action/tab.ts +++ b/packages/extension/src/action/tab.ts @@ -21,6 +21,7 @@ export const Tab = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, + pageUrl: location.href, }, active: true, }) diff --git a/packages/extension/src/action/window.ts b/packages/extension/src/action/window.ts index f73dee71..1881a42d 100644 --- a/packages/extension/src/action/window.ts +++ b/packages/extension/src/action/window.ts @@ -31,6 +31,7 @@ export const Window = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, + pageUrl: location.href, }, top: Math.floor(windowPosition.top + position.y), left: Math.floor(windowPosition.left + position.x), diff --git a/packages/extension/src/lib/utils.test.ts b/packages/extension/src/lib/utils.test.ts index 18431c52..113a6181 100644 --- a/packages/extension/src/lib/utils.test.ts +++ b/packages/extension/src/lib/utils.test.ts @@ -199,6 +199,47 @@ describe("toUrl", () => { "https://example.com/search?q=hello%0Aworld%09test", ) }) + + it("TU-14: replaces %pageUrl with the encoded pageUrl", () => { + const param: UrlParam = { + searchUrl: "https://example.com/share?url=%pageUrl", + selectionText: "", + pageUrl: "https://source.example.com/article", + } + expect(toUrl(param)).toBe( + "https://example.com/share?url=https%3A%2F%2Fsource.example.com%2Farticle", + ) + }) + + it("TU-15: replaces both %s and %pageUrl in the same searchUrl", () => { + const param: UrlParam = { + searchUrl: "https://example.com/share?url=%pageUrl&text=%s", + selectionText: "hello world", + pageUrl: "https://source.example.com/article", + } + expect(toUrl(param)).toBe( + "https://example.com/share?url=https%3A%2F%2Fsource.example.com%2Farticle&text=hello+world", + ) + }) + + it("TU-16: replaces %pageUrl with an empty string when pageUrl is not set", () => { + const param: UrlParam = { + searchUrl: "https://example.com/share?url=%pageUrl", + selectionText: "", + } + expect(toUrl(param)).toBe("https://example.com/share?url=") + }) + + it("TU-17: URL encodes reserved characters in pageUrl", () => { + const param: UrlParam = { + searchUrl: "https://example.com/share?url=%pageUrl", + selectionText: "", + pageUrl: "https://source.example.com/search?q=a&b=1#section", + } + expect(toUrl(param)).toBe( + "https://example.com/share?url=https%3A%2F%2Fsource.example.com%2Fsearch%3Fq%3Da%26b%3D1%23section", + ) + }) }) describe("matchesPageActionUrl", () => { diff --git a/packages/extension/src/lib/utils.ts b/packages/extension/src/lib/utils.ts index 7aed90bb..1071cf06 100644 --- a/packages/extension/src/lib/utils.ts +++ b/packages/extension/src/lib/utils.ts @@ -84,7 +84,9 @@ export function toUrl( } else if (spaceEncoding === SPACE_ENCODING.UNDERSCORE) { textEncoded = textEncoded.replaceAll("%20", "_") } - return searchUrl?.replace("%s", textEncoded) + let url = searchUrl?.replace("%s", textEncoded) + url = url?.replace("%pageUrl", encodeURIComponent(param.pageUrl ?? "")) + return url } export function escapeJson(str: string) { diff --git a/packages/extension/src/types/index.ts b/packages/extension/src/types/index.ts index bedbdaa7..5b357c44 100644 --- a/packages/extension/src/types/index.ts +++ b/packages/extension/src/types/index.ts @@ -304,6 +304,7 @@ export type UrlParam = { selectionText: string spaceEncoding?: SPACE_ENCODING useClipboard?: boolean + pageUrl?: string } export type ShowToastParam = { From 1ada675190246814200b227bbbdda62c79fbc246 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 9 Jul 2026 22:18:39 +0900 Subject: [PATCH 03/10] Move: turn "Search Commands on Hub" into an editable default command Previously injected via OptionSettings as an immutable, always-present menu item. Move it into defaultSettings.ts so it behaves like any other user command (editable openMode, deletable, etc.), and add a settings migration so existing users receive it too, not just fresh installs. Co-Authored-By: Claude Sonnet 5 --- packages/extension/manifest.json | 1 + .../public/images/search_command.png | Bin 0 -> 26723 bytes .../services/option/defaultSettings.test.ts | 19 +++- .../src/services/option/defaultSettings.ts | 91 ++++++++++++++++++ .../src/services/settings/settings.test.ts | 37 +++++++ .../src/services/settings/settings.ts | 20 ++++ 6 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 packages/extension/public/images/search_command.png diff --git a/packages/extension/manifest.json b/packages/extension/manifest.json index 872521ed..15a68e3a 100644 --- a/packages/extension/manifest.json +++ b/packages/extension/manifest.json @@ -81,6 +81,7 @@ ], "resources": [ "assets/*.css", + "images/*", "icon128.png" ] } diff --git a/packages/extension/public/images/search_command.png b/packages/extension/public/images/search_command.png new file mode 100644 index 0000000000000000000000000000000000000000..a10c2c60a03c9cdfcc6a659b014ef637f4945e97 GIT binary patch literal 26723 zcmZsCWkA#4_wZ+oP8CMyM7qXEX>f>2H_{D)fP~Tv5!3pb5Eah&pG#{zV2-rN>)k$0BG)L-7*9KBJfutfSeTkaS%9t3jUz* z(X#LZ04n-_Ul1TGhZ+1Q#Lw`yI#4~vz6Jh4?5d`t1^{&lROj|2;NP_VD06=!Zx{b3 z_P)-5rn~Dy``o(MjsW1gc;}XyaUkSR2U&BmbHLGsYnjH5HM4P6AzT23y4uFv<~BVm z{lMm)wsxU+=oj7+w>Vav#u;X*Lop#4V+Jb2x8xdWHl)lyC6+Ee1?TQ~#uy#Sw^fCx z5S9X}e$Bb&`MDgaDEl9cTb|hc-lDit+%S}pKGFC zGtD(89l9o2##))+p2AC+S~{_RB>4_|L^1P4SZj=Dk-23P%)$s418-tzDaXd6yu7NL zWR`vehwblIWG^B3lw^RBT6>*`rcR&WmeL7VWoC6kI-BNhK3!eBRnRZ{HBNG=Y@>xH zD}u}7>v_bd;L9cFEQ_gMf3>LCkar_Yl8i8+UD)*49BZd4?Q}cq-`2QI_-%$c+d44| z$6rV1R&8QlJ13`y7P&iNv{B0?>zEZEN52jWWK920&1&yz=4%yM!oJqy`!ffj4~xD~ z)35<9%H~`GD-UCTn>#&+D+tOAS4kJ)njUs8z6+v7jq$Z=Bku-vu6{X9Xv630)%`G;b<-}kZJY{yQ;vRgROjRs;~yUSI6nUB%f`4LdlbQIdL z?Z;fhOgke)9bEzA6y>y?vN~CGMu~Y>n+e6yveCGhdIJ7TG71c*VxzF0zb#>Y4@m~b zC_*(xbNYkY!~GWWTMm`0-K`(N9s3k-+mp9j*Cl0bMKzv(h9{UbpY7d!_%>KIiQc8T zapRk^SMguT`T5W=o$F@OGs0OPu{TiRrRD^gYd2reptnd!5+s+ZTec?@H9xpl=ght= zXc9ZtVVk>!tUJ(|rkNE%5OtPI5RIi}@*N7{`B9t&a(|yVTQ~8%ka=Psu-{B0edTHQ za!37DMXIVJz;Ok?F3(18ge8@1oZ{o6$qRE`$sVMmuc3cDL-r5lIWwoZOjq% zWi_^#vafrZ$|Gm@iw8#0^3hMO*A-o)-`KON)=yZz9t({=yQ+UYbAW}7?g!F{nKF}A z*`p&@YAuRqq55>8rt|%tE(%O@jY0E5#{o3JD*n^jc%b02Y1osb2r=IzPvU7x%>^%% z&7_vj_N8_{4ncb#iuTC%@ImcooZ&vNBL<(wKc%eo#Zr_qraoayx!qDUVQ|{UG-tH( zdA>0yTqD!IZ);r#rCdF`+g|X6B!sQ{k{=dS)T<@aN1~B=M!cqX{QG5uQK@QKa}?nZ|jCp{6O zc0qEMnJ(L$#Pzpbf3Ty6&@0gj$+viQcLXlNF!N1VH59Eu z&cUct4^U)9-Lx_8B(5sz6Ty)$cVT&wz(gzXjGJp#i|PyMyoQP(J{ImOs7~@wv;p5%RtHzanfUSm;?7VnfK;n z5(sW5W+pmVl&ixIjrtk$*)hmj*_Dtgit=tGMW0&?z&~Czel?5^EyOB0KZu+n@SDuA z{;JRaJx8x%gtNeOd|uo_EXxM@Uzz&&hMiHRfk2L)k$52Sx5HItf2j?)?avcTkM)u;z8J2O$U-h$Rvf9$?bj8svt;>b zHJ>lw8fSP`gL9 z$8vw@ZFxJpYF7KZ;z!;`^)$6;KR%B8keX>KXZL53++P0B{!~f-&hB+o?yElkFb8P9 zirm~(9N9pGO0>d*=<~~fmyhL=hiLL9eKW--bVWMS;X|ho3{I_`NQ%4lu8U1&_c~dV zxTD5*Bf?c*Ujs7>zK>}h-FZewZhrn(gdXowCUSv_M6}{8*#Khn*hjQcC)iy7C@JLS z6OQ4;oUk0Oo5KqNQzmlx97Wi1xb1U8_4`3Tt~RT#)6N1Jkw-)`JjEeS`zlRX>$Sx* zK0DTLegY+Gp};paMJHOLHUb|0d?-WVDcdAs z;JmiQmdBCErJDUSb_aGMi+TktqQ8IHd)+4kIB?UMuKt2~UH?co>X}o)?B?L>cPA-L za(&FFw(rWZW>S-Y%hmaO+9_w}=gwLmJC7JovI?uGhnGf3F?mjf70&fhWa!zQ0dPa@ z$A*~*BgBs=pB=uR0@fyhImas1o5%}o6nS?>%W*+3AQ6CWTK_2N;-!D;?>#_%Tn682 z^Ya(&iD&Gn5y0O^wda-aY=3iIj~XEyG0EE`*?ZypTs3!I72$YokSg!lN`A)9K?9@r zI2Q_EO%!KG#ZAn>g6a#)=}NUqx$SPK4>Gq~&$Z#&l^yf3{K6Fnh*?hOmTxwPiT@87}fF2}?#a~$yW96)czBm#ri zNr7MGr}fM362J!$ybtOnm-QQ&6vTcMRYJ4!%W(gbGn=FcQTz_7IxW+*j5;669>jqo zsWq!ydZ9$*!ecIeWc)01k1>~jgG?u9gGt`#QL?2+-2u=1`n$CRSxI@dnOc`kK_ltLwl15Z7c*X>QzXqNFFGFib``g*#M9kr%3j@LH0kAobN}mQ`l~JF8z6Ss!Pl5^8pWUhOPRD}LPh zFnlqnd4IrL`|*7ZyA~H}(p<+=A%qh(nZvN9jIH;i;$myd2ixmXr944NO5-so<=Hl6 zLtNTB3HRm?Bm=*0^dwAnjHfAt##8Ao=De1do?`ytJ22fSbDa^D0PkvTvH4;(-7C2k29;Y{j4AIj*<;F3GZ7-J_UEq2<`t3>6{V%bxHjcfp#UjULei+!Mk%% zYqt6%>*A{Spk{rKgxKsa2_gXwzEZa;A$#Z-s4Own#V9J5TbN+qOQhLSG%N1KSu|4f zAlF0hcMjIu9n2SfE%(_IW~62;aX98F>-&9-F#%lf)2M#QnjLP(2XlJQD=wA=%?C5M zIZb(ROK{n{jxy7^_ypD@7%*FTi~1G_$yZK00JWP(VtArx{E~qSB0tq&GhM28>jJMS zX4G8wBE8A$1o=Ir{~d=Y;o9Z2W2p21ez%UL z$+Ud`RQ$ff=alfwAr~un*OS=k2>4wjccNl#dfB{U@Vm;etNFIMk1$MW{EeV@918<2 z^+W9Lbnb@1!87thV`d1LB>o;}CSoT3UjCgFvO{vSLg}q_{#&O*VEO5(3M#i)g1Gfo z_SGf6)w&n@2M47pE!ZuLnzZTRV8$H{o1O)R2J_un$-7KH@@d2qy(b3V5oeK1@-O>g zE)+N{RIZ2&Z7>7{B(;^X7HUb% zx1VsDP+B8MnblFTk9r|nE@V#US9~~>j3&)jl(W3_Hy=i^>7Hyb%GY6=Fgf`?XKaqz zqdun}6?~k){2>n7OPu|VEf+%T>n)5|@fG{}l-(3p`cu=}i-GV6b*X=K-DfTy;gV&a z&<6xsBzX%DuRr+KkPhFJ^#8^qxnEz}d%qWE^XYjTLyzbIKWe+$=Ov4y_Dp^k6@}ly z6Z^Z4Iw8jnBySFX!9%bkl-v2<0`rSgk5502>cwK|7=oU`)nfWH#0GjnA*Fw?mW+Ri zy&CA0L?<+yRXPcc^E#_7M5Js*qPpEolX)MUsxn=hx6C=TwZs7^=P14NL1sVKS+*gX zd<>esmP;trMJMD^UEO&m0!!9K;L`2UGcga74olppSZI3TePNIk1#7JP^@-VWL_LvT zHKjlUR`7X8Ao;Dw9ASfA9dh6sgt?|&|K`0R{6|C=Jff=h^Z^;&;8AR-@g&91Hq5#r zGqHzr9KwaC>=j%si|co@y?xm$+a;Nw6JX9u^lUjYcc1_hImK0R zK|Jr5Fm=rqLJ5{XHuN1f9Ts~SY_Z4YIAZA5QN96_Z)m|w7e@t>KX_c^h+U@1x0v{n zbtT2PRqBjq>xT(u$Vj?Zz$)u7vFC0)e;vm==KD|55Q7;#cH#rq(Xwe5u;`)k%p9S1 z>?dQc_H{5ykiD^HLT=RsW1l+t!$-Xyy!c)z+*~HN@MAUNO>dT8+!C{+CS8cA*ujkO zY1uN&POtjT^Y^}nNzcnP(jex_QcsV)zx-8ed|Iq&xhT#ce?Q=U-XOD)!ZU!gn7pN) zf5sevUcu9%X1?ogUenb6OoF-EC>^FxP8BzM`5nes2I4lPjx6Xh_&qAk3NaIE?fJ?| zY0Hpc@Wwjtt5nU|5zeR!G3fME>0}S1)H=wgZYz#=(~;Lc3`u|oRhQ6h+(bQHT5yU( z@Cgeu+$@oyLu!8N68g5Hccb59F&`UTxu&5uW_a*!Q4_q1*GCDzd4zYGnR)Ahbqp*j zd4F#i%COCny!qfnwbKZ%(doDrLxBcg&?53taZiSWphmiGmEBhoR zi%F7l(oSRC4ud26!q7Og<4<(QDIbczXNsX~@s z|8cLpkW7ZzQM*IJJrURHX!3lUhiN%*V0yM3FdO-3{856e0}@8B1@&v#~+@neHmiSjMRkPHF)nR?UhTT24?YPd=g$U1klk+kS{antwtrGCHo zhN(!Y_G!mVMl#A7>k%xkG>W1|7@2Ndw>9QH{|8#vrz*ncgpD-mNzBK>pX;h!HMQ-M zA1{N79-Zlh(UXFe3c~l1KhLx7Y>x@0j+j-Xf6%A?WRJ7|DjOKE&&tKe4?@vqva8`o zTv!gjq4x>fXDu(RcreSp7h9BihSRfpaYtJ`&Eq^CQF5<#1DUeEF}#q;sPxw^TDK~O zBem=I(`Sh)CQ4xxU|ocoPII@j1dAH3s!*<;C^xB9OCl3`@=;}8S%C0h(HO)<upUA!SC7qR8r{DEK*|*ZRqOa|G3)d{do#MSXeinqy4talv1;yc8zXs87 z-PJ0}E8lAH1nqSwpDNr0*Ma%m7ME|t^=_h0!!hG4u@~k7b6)2=CdD_k{|D7svZ-SS zk^#g=R=aU>6@;eQw{!L~q*mEh^B_i4l3$^yCt8|(@fYwsVc*E!VuycgZ$`dq^rtv4 z@bv{fvOw_fw?C9o%4MN-^gjje%#+y=S^lw z8fkRH3e5O@jvM3oQiIG$gNduW&~P1j9>vINj=RU<=A^(&ia5*p+b7)RKi;6cURx7D zs@F+5BCG9$MC2h&f=o}0o_%{RDHI4U9f}|mvh8xvC8Ss?RY1LV)#D6@E`g2qCOnrh;mA;^eN?P7dfVZ=Gb0XT@_w64mSq0Y<`nmI7{7i&ENXu9(; z+06Y5sj(?AQq>Q5ntyO2#R}5Sz0+blU=+vqT&_UTn#9fAj}17hMe%;oZ6IIbSV*<0 z6sxd8YmkLlrf^}=BsFRvnBaH1WE`{%EsX3lkpx%8^hr~ScW(_tTbVdQx&B+zdi77G zV&d)-jBl7KxS}MA9=5oqJ+!vD3~jvi8l*dNlXH0O(sh-K6CDe>uR-ecGz?{fc6_88 z=`%9BJJyi&2Y~)zlX}bgwY;~xKqYAI%t$K%3S43k29FkVk(*e*PjpWj*tT>ESrZ~$ zKa9u%p=3->tY? zFJug9y~OZTsBBG8Y%c6pHD!uzcjQ%NcZyKK8%$uTX5hrXwg*`0<3gi({uyQ*)StE( z%_Ky|K740l=^>))y*YH*XAML6@zesWZvJD{9(v4vbDjc}+DYxed9MQva1(G5A`~(F z(EO)9Kh|!p{iJMGONwILr;0@YZt{UX`*}|2&Jku{>{1pa)k{m6kcQVL!}+{|)aGge zgS|r0z1G>PW7F^-9RFB-Wv&A&KU_dRIeH+l`5oAtfz}ql&mySS+yVdgoO4%@6kmOk z#9~p2)*ug5=_1Y9MRYiK->&lTKtgIMaUW=xN6S}EudfkVEBQn1BG#V1)IJ127G*MQ z=I04BZT{>G{(Im7h!}A~(dJ}x>0KtgM64k0Dd^@i7L=EOZ=0`#Ka@L#3@9eB4X}Cr z6Couv&`OkBt_X7nw`cwR>j)$oXyKPA*YY0{eyyv3c=OS;lD|p4!5cOM&n{1RfDWey zeqOpm+S7cyc}wz!!Sjb8Hw9xMwef|Dk9u;IE|`I4!jU0oU$b&=ge zTyHw;Zm6n|c1x~F0s z0MjXi`q|nr(*L>akv|J^LE?+YM|cS1TqpK%eBwe|z&^XPGV_M{$q1x$`-#Q=dBA9w zpWpRqe)>G8U>4*^jX|?83%k19pC>3W&(4jltU(bPeE{}BOf!Z0N&LepD zqtgMlFD+oJK(2Vl7?+E&SU8hDM~Xq?M%+Tuba7N!ez>Q!D`E2b7M3O zbC$4I>MCXX1BLxe^d;j?Z&=a*;C2t9Hwlme)mWue=%Tq*YK4=w%?D%VQ8FoC*@3O_ ziuDk%u6)2I%*A{?Te@B>kWPYJg4k5rZ2Pt%2{ZJ)`VBRvgr0|zB_Ydd*GC3VZ6z+1 zNp}UVkv0ccb8Yey*Eab26!pGYVJ;pbE)9sog$OWQc4FA*WR>|N91gR~!{rR#5B9to z^_!1nhMLK!zZ5IYPopeaPDhEoMl7H;exfLgRH7i=J&uca(a|=%T{!~)cjgum3(`XW znLxn2NB^$;pBlGna(QHS8QU^qw&bcW%kH&h7S*k$TnKRuz)6%$FS z$gaA+5PP}@IF$o-KYY%Yw24xcW$9ll?VW{4nNb7Ok<|j{(aT}qtdK#pbY@aoJ@l%9 zXvW3)KH*p^KD`5}DKgGl5lyky5Id+H{~A#7Q=J8=tnVUl3`1?3;FM9@RdQQL(%#?C zYsmH{=Q8yykpZ-Ie6%_&g1FVvHAV_)fZ?!u&u0P=RxvX}87p;FaG49wf%2uH z(3Iyfcf1Ah7`UwoGgL)CxA`4nfAWwhpI1TydxhhbSY9Jjw>nrr8-~2W{=Ss$d)nXW zDmZ`cbl|$>@9x4KxpjgW-#mtM=`#~ovrEkN7{!l$(@Y`bPIgQ zw8mJ@%Mq#|;DaG;AhISEaS^r3Pj$EnPcU-IMqdAp>C*jX@bUe97j4Np9px!;M)^Cm zG#QE$xCf2C=*f0oI)JqM@i4@@L%OcVR0<$+|bx(%##<*n`) zCjdiFT|dR3GZmi!^~}KLuKc}|4J1&@T8{$~A)$?NnOusdWI_&ubV9yvNuo|Au+!9C zmYqrR4&qKTT+=Y}^%sA-cR+nAQ9b|I*58~@K=<(Ctsr%P8JNLl%&qt*5Cy*IT^N_X zM|KYxthLQsmoljHjJ&hhP1dufAd@(5XzVjhqK0BC`E*yDiCH5tzU&L_`a^oEi z$ZzAbcA4kXNRe$2fO3D@%3~VM-jZJ}iKew|K3~$u?P1Jo5gk`LL*Ml>2i{I{gbi6V zeae`uzmL?YlJtk-BocIO8@FZGZ>~6sdRDV1e)(0xRTABK(I2Ez;kM$j=DkiNezhoj z$r`7GXKmK851$OZlO%EC^d{i`rB&R;nC2M&$Bu!G=Xn=V{tMQ zF2?x{^?|@QcC-Q_9a8%e&kXcmHkcc_oJ)*6?NN%TV4eqW^gJ6cU-#As;l(Ly(EO@d z259`aAq84C;%k7i*){I3>aXJ-EZ%x@^plTFAm*snV5j)J$>JwPp4zGdwprvWY##aYhm>OLaCo&Ib)G7z%Ke!_z)%tu5$9UJ!b zvGynDHoC_BI@J$og``yE+TiFIN*hExCbY#LYLssXwceF<$uRBpa2oksKrImqk#L1| zTak7T4zEG5k`_a8w0y@(MOcD?)X5{?*DU-8+NEhyocxCaKNxhdUxzaWQbft@3^)O5 z^xAM_1!y+&HF|=0Gv5YAq`aIUhl!KxK&M~517&#|S=K08LrL}csJ|^mGr)W6WfQ?{-xugjs|I_!R?k9F1P&$Ux*?uuRq&^Lb zV}7g!RpQydY={3|@bR}X1T_@04p5Z4AeJG`DEZm4B!#WBIT@SEXpvGxiQpdJq7YJN z`TmVLI%xMF^NpCs2(USW5)<)#lvKfB^rzbEnWv-V*Ox|9;i+#Mzearw`4q3`=A5mG z_2=Jsdh6b)aHU)B5yg|oo3~E9QHzGSA1JYO1QnRI%%`VxHr=mLwm~ZdPPrn1^*APN zK=;z3QUJ-$yt+RO*L$57L3O7ZEWS5le@2Cr~EQDQ!EFU=1 zb^H6+B{4C{-w)E%Nr-wZbL@>dZ^n+t<`l@GBqa}GH2EwZ#1uRVyPhsH-ssi0BNPXr zh6>3Xv+u3ijr=L!?jJ-D3D7yokeu!d6K#CsUCyk^Ds zUQAS7TO(X;x&1OTTZWDs+VJ{t+y(F~Q+%Yd0Zh|HnSwRq4?&8f`P5@#z8DwXQ&+)j z$0Y^nBn2;H*&?}M@@fSPmOR?3v}&(~3hn#&?!H{6V#rp|M5m;VdOAr05{oWGd#iBTMl zw&M#xT+lwzOgOI18!H+$`o>+!jU~gR21L>6oh|Oxnmq6~vhd@#w;kfw!%C}0;@HYZ z=Qo}aG*COoQ?MglrZh+pZI>yt8kZ4{T;j{s1Yi)j<3D9=pqi2T?q!z&&M!y6aa*gz z;PCgoC&8i(3yJ+7nltD6`NAC9J_L!Di0K~+pl4unI#WgGE&M)PkAD70P54ZB*9)~H zN@g*5k%I|}*d^7+&+*Y*Bh7k!mnd%j3>Fk$sGWF8J!T;(IwP2Hsa~^G9vUD6MWaHC z;mE+xkJ@9iXB>Y*imo1Qst7$8E+r9hOFq&+=Muv^4}>|alR7Rx(>xT%SHcqvag_QL z^_?ZqWFiYD0*@@EB+{y!J%l)fsitV>5+*L{o9nVlnvy&tH2<-Lb5rU$Rp4h2*9>($ z+nUd~iT@<>-t*zeCLvaiFS~O`!n{NdYP!AgcOih<>H~L+lof)!;EX0aQE2OKeld?;#vI^tRJ;Q9FyE5--=0IMijp|tFAKF zQoMKu@5{dS!M)0LoG655w1Bg4GgYg^SJcC~nkKzKjchoB+#Y%6Wccee8^u{7f30q# z-tc5VdQ@D=_(u)tu{IcNgUI=0D{0g1qUxb{VkU1+hC$Y|6gMJG^+ZBs?wK?luv1IZWrN4aQUBA+D+#%!@Dh7c|NZMV>~Id zLzcXHz4(op<8jA&jPxEBE^d6FS@ST;#X6!Cdxi-`t>BNcK0(fCZCDf$2R7a0%YdO# z;7fhL2H2+m`t^pSHL3U&O~8Z5`sQ%sNLbV_uA23Bdf~Gn;~OzOMC&9|pXrajN+`6| zZ8`8v8vLn&J!<&wRIky%m8xP<-I%6Smuun8W!V3*I&FxQ-z82;Sy(+uincJ;PRApD zmqg6Ay<8jp>A56dO?>SsW%MP-dJlzle8J1x`_P})32&DSVOHe)QVDm)dOD=3 zsYuhYcTwYzR2A7aYv+@m#*=sO+ZEN4@?PpPt8}oFe1{kD{4HxS9b`;5A8FHa{#LRq z=WI6HE+qHhUR-Lu=P_Ns-HrKY z{fO5$Z)QYL{lekkgsM&I$4S~Wc6ElP>P^_Ta?Tl*UyLaHIy`Sm8_Jr(6(^M(RbJ=X zRd6@|HEqE>S0K@@U2R0GQ2uuxavt$#Q0YaU1A(={Umorq*T4D99mTerIkM{MjHC;i zn@b*auJd;3PjO%RSlP`3yaE&FKeTjaQk%t86<>e*{MgQzM*TeADdVGiFzKW}Om|w` zh?in!+K(t|pD-MDV}yFMlS}}gn8Azja;mxSW?kel*|KS)m=#eWf%!N_iE5HsEi-e! zWYWpW%iQud)Sk#1oZGct{)>su3Tm)6JNXfOBVr(X=U|t8^uWE$o}=EKewE$zPGdQg z8sZOM9Qtq`*4g-sEV5Q!-aO<15L5o_(W5}_xKk4;o6NgI{~&J$ywN(MgfwO&u0^4DOL%9Ah9Y&BSw8Lc*#pPXcfsom6(bWxuL zB&gJ=F~I~T;Rt%hWcsZL9n(MJYcYn_ktr&q07YKEUc&})?M^cPz&yp*%6MbsRZk$} zgqsmHwBeEE;i84%K^{h7>gqYWKZ{~>I-6S@-fvgWzkC;`>Xp)3or0k6CPin`=^E;Z z;mL+OO!W_W&k>_Lw=7Pcz*M>>J<^9G$I$*sVWp1aH+$ z40HE_dcsbX28Qy6!e5m?W{U%cyjs{;=* zwj%})2EeJHS9q@sYy!hVy`oX?2$=^?73HP6{`XvMz!L`63z?+^PZUsmVT19>1wy<* zf5aS5pZ5r|%=ifhLUIKi2VYnkWCh3$Mv1IZ-A${42Bt$)9nhAn$ zhChhCNXj+EO<{uF9bXd#9FAb}$s#nbUxNZaaCZL(n31o%Ztr(~sDwncSA^~B?Ba}` zW~)iIWwYTY!-M2q9!a-{LK;}0HclmLJ(#K6A6`Z@d+G_{moT|ogVdB^5N4LJYJ5*( zzDDZQ6AZVGw=j6qN#W4P%Q<%d*W|_%b;Q(!mp8R^3Yh zGX6~WY!RMgrm|8BM(4J<9=PD4igdA`$^G}ABB>EhCPb`(y+ET7G+-I`p)pm(20s=1IfXRtLk|N zv8grELKffX(h)_@`*3lygG}uE%PsT3oH!PqLmzj z2cOd?Q(Q>eU7{<#rx;(gJIj-jF1KcZO)=`TpbR|Cg}sQ-9SbvT#$NXe_fwtFlAs)t zVPL1KjC6ON5$XSLzIHxV&scf$_~Y~kOQ(_JZYtH;ty^o67nvsj^UrMxyO>urXs_N4 z-Ko4YAtr4Ph3Xboq^^K;u}Gp7+A}`#mGRi#e2Fgzh~6RgOvrXah4gYF^N%JAGF*#j z#0||^vP&G;C8IGw^)7q)GU5fk`(^C2t|LKP@5fUdF9^2gxL?Bq3v5F&WuJ_}leT-X z28sZkzL-{T;Z9e`W%eCk>m)sf7L%Zx`t^ECdWh@Y83p#x{1AP7)5D|?0Xl!BvrxZL z#t7+!rxM+J!?MArJ}Bn@t(iF3$i+ygGRg&T2s`r8kqW>BnBQjh8h|1wa0FqzPZ_LN z{Y?--QM^W29E(Ta7NSxR9(>%!KJjk=dI2iC+A|Ez7I{PXeJY!oPrnv4{3YROt@Vcw z3nidR`BPm3z(OUf1XwU>j~g1$L3Nto9;`)9ESO5VXn`VN?7w(oGQv6zlnQHJ&4WAQ z)od}J(#X~ygHBVUk5siub#JFwF>E07@W`)aFJWlt4ob`@*H?Ahni?1eQ3&=||V_Ca%kHH97NAgSsTZm*0RK`-!KOG6nqyUmN;OYMK#B@chT|0N@w z+bIAnCPJ2#0T(-vh0s(2-*8VrJhKsfX1i&&)au1|>Ss*)vgXx_oM<*yLv zh=^(UB}Dg#UknCWw?qi~eK#2h-eu*wu~j$1pStIfw{)6dp@vw-2`*?f|r{J_a47;7?@T3DThf&%XQ}_&OTO%|=1!pv>xM}3$&LWC zw}b8i<7qz#|G7KbCLx(uBKCl;v1g?hg{Ndgm->mTMJY2-;WTmu)M?RKxiKT!4Dx2z zqy=Yfo9$973iO7so(i3jJZNDgb$wK6(65{}Xejt0twCy)OAjZ2lmgjARsXz!nuJK# zlr9M&ueQfksfYy1a4lM(2LE$mqF9Jr1@_@Egu?X5>=`s}h;%O^3wT(@UMb>R4A1-c zA2&tGAx^$LpUK_CQIh$Hxxfs(K*VvHBFO~xRgj$yRfUmQa0;N5@NqaJl)Ze{(0PSn z?!r3;d>XRdL+YE1?`wp4V$kAR+NsBEN zny>mblrhx9s%VTTdjMTqDW!z|FO2y-ea?zfp z;!`x6_tad6l~QBrVRz7+5YL!@hF4#Lbw=r$h?z13)%yOmkzGJ{Bz~RNGwuxAlaoJN ziUUAqt;??=1C+`44A&8CyYyW^=V%Pg-+8kkSU&9{vkv91P4DG{soTzvtA~KBv;*# z_9A#=>mrcw_MXRVGOud{m(WRMu$CHMX$P=bC{0K(!vRiD&+woAWn5|_DUwBvL%1_a zjqVHsb>RoYdPDW&?=N4T5Sp>4|MDTI9W%9mBwm`OKyZQw^#RPwq5=+?*D4fSj2{!S zuR21zCm6_Of53)GhRnx>d{g6CXe=3m-og*_9*V!!PUp2gx|F@%M~b$OI=Pz^o95jx zcY89!*%nL>cO{gJPkeQ>F0Y@dSmx0pok8E(t=adiyCRKB#>JOm_U!^wH+9Mz@Cg*o zW>^-DnIcg5{^?>t$N;kGue{ zGWWr>!hg*}PCeo=D^$t$Wca=I8#`@f9r^g-Pq3nh@nppHK~l84d<|9{V^thF0ZVkM z>Ps89_W0>g0S9|mT9beP+x+vT6a_*i!VcQ~Uf-*Kx^619=77Ao>t50ZZ0;V|FF#ah zzw<1$UgsRD{%Ag8DIMVmhLkN5=rT3R-A+cXYLZ#Nvg@%CodV+t61CA47qT>m3E2fF ze@2MAm4+#Jjm+NL@E9b0sFJZ6b>L3NU7)H>#sYM%S4SA}cCb2*+~Qum)I~9K|I!S4 zRp^%;4@`+SUo=#2eXR|v1E(o^mr1zk<$t-n4870E;>j3uc{T2|_=EC(@Z}EtC=ub< zAH_G~X;q4ST|%Mvwek#xnK}6d2X8P6ecu)uv2Cy$5f(F&0fan5?RBf`j)Tu=03;hP z#OvS7qr)&w8Ik(Z!dM=*O;O49S3dZNmfoU9tDuwCUcmg{73$yO9CZnz7n#adL3+CW zkBUTgn<$xTKqPyI2rMX@s?deYV0(t62m#hd&e>S^5_a%?oLJeNK_JjBi6fPsnm)MI zXmLR6N30XmyQB}ocm~wci#8$I{>#kMCz1cVUgn=~X%4#F-A}D#IjXde18lfT%j1k_ zMvv|K#ao2Mg?%H|w%PBj6S}~j}u0V*Xpdy6Mly}iN`t1P7CN0BdXG!a)yj{;>LGDQ( z1>$-~CjRb-!!#n;&twPR63?72qQc+K2TWHTKy~e}b9P@OkHb9O;^E5%aXS zP<;zH#v$Kt648a#8SZL;XpLpujO_8Yx)&3+*ah&a0lkxFsbr>U+`N! zlK2YAjnClxO-{Q-5dXVTJ}Ifw$4OOdD{523MFlnb2VrH-qK1X1`rtXmze}{pE7zvK zutJtZean8+lySUM;!Q65TP(>(0frTa6{#qq;*nSD3fbkfFWT_I2JB|S zStQotM$q3z{bzYbxD(WNdFwlc4D%BNs*CX5S^F9owu{lL{p98and=JDXnyn+(qZs1 z=Zu?+Zu<~LBcASGn_W-DSN02uPC0l%w?`E}MYz{O>|RsyyDy+QN_fbncpjbk>&qcbqyAP>-0C69%R8^rA|m<4Pb1!9#8HuVFm;_m#j5CO2Q_^`90x5~z{ zN~wlPy!(P72GAT8l2`MYw?qFalKV?YDIANq_vKSM18KLrBp;aMkK`w+-lpcbzU=cw zVVT$)&`5Y?8YrFdRnrFD0V#^kp=MCpxyWXiIu|&q2=l(Fk#wQAp2H& zdR=6nB0lPsv6AyE>8JRQYXqTa{_>K)b%|H@KT_>VfLcIiZh(99&Z8(-Pr{|MdY7^} z>bnn5Q65E#ijlR4L!BmI0)V%0ibv$#)bz=3Oodjr+A6~33JfuaS5U8@5nN%=-=D2- zei!(8Vq5C=vq61IfieBBM!BGK{Q*?Y3>+03urmf5tRi0m?ptp1-nQxXhFL5myykBe zWxS8*ss*RKH5pD|(p8aa&l;onmtRWqT?Nxl169(CElN~N($Bo!k9TOJ_3Bx3ff!1I zcOM=lQQ`R1_j|UE=1?})xN_8?Ss)2R0gTUvDDQi{@_>I+pTp;Vl2=bPk_%&L(@6?SR%Ayz65SIyb%8!;6 zd{mBqD#uaC;PHEFk?x_+nGNL=+1$tl*0~QSoSc!_#!qYmG9kCJWqa2X z;BIXE&d+kltd1-N<787AOT+jM9(|6;b-25vJUZ^bFwn&uSflmAiQ@`ozGSZ^nnif( zeuM}9QBENoEq$aFz|T_2A@@vCqi%?BYZ7>FTGu7yTilE6KJt3#G#>*Q&K-BXSjX6v z{jQX70!NEhcKDGZ5=6Q4itGC&XKz6)Wk5T9gomMYr0RY=e;2~C@({nQ!3yyuHWeD? z8%oc=6$f@#U8{7j?)ba@tG;sM+wkdVK-BtP%&6mCb_(0~UubfE8k{P8 zio5o;--y8;UVGJU@Sjw?I!m#su76rk4|3igxxXzQg~{n5Z#br!B;V`FnERA1{D#8J zV=$yv3}~L4mzRz1gQ^&Rx!t9j7O_edXkf5Jqqko+9^P8N* znRku*vu3_*`qB*gSawU?@p{mdlDW9QW=K#{I+mI$^+j4l$MU3n3zi^^*U5B$eue$z z`}Z%EZy}YC-*&CC%f`bNej3I>`~_FkzmgBGZh?cb}Q_wcvd4j5hr|2miINyja$uJ<0jmsZso1jYV$3}ya@ zRoC**eAj+ZH0Lp@b#Vf^hXMpy;bfD?6U$1(wGYZ7AD+G2k4WUIH3diWkIe+*?#d{M z-2WO)qdw&v>`Lb2ZN=L}lS_)tJnI9eL~tW}fAeh={1IVtS~%y9M1HhcSWk@Og#g&| zucNTKr2fm@5SWpTq}`9@yG zq`v%-AtsM#_?8u_Li)=R&JwuV3^HyJ02*SNh{|v9%GvK&@+Oe7JG-U|MOb( z67Q7sE^*?>(|fT)k5Vh<$R(^x9XAN52t_r}PbroCzqZah zkgEUx(X3R+43&FWRMODrhoLF{Q&Q{^z%+&2O&)me^nRr$L}Vs|aSfpWz@C zH~Ho~y)ZwhK`rl>>qOQb6Gia;yD|MXYG|z>a)@nD16;!c#9pkkbYkqnvOD`2Wmq5N z3$kEC{!0^C|04w?J=H${8zQ5aubg?dB$Hfn_YSf%$O_y>XyYX#BRuz+e?E%}x$<(b z*pS=FhUZ**n=F+-j){S|{b}nTm6P-}n!IWZ5D6J%pqkRblQLBI{Aq4bDoJUY;_q zd99`)w&V@;*D{BlUsM^3HAY>muBYjrT*p~S_uufS!tY|u%&5_clpn{f7x?l2&cOpU z;@i|AVovvKd2RJZw_`{{ofET1)rT><=_qP-dG6ADAPLh*k>Cl6xE4H{BvT+Low;_A zn-^NU=)FM3nICS%G*F_Zm1@9ItZ&LLqtQGp`%rXpF}NgF2!1@tm!Xe{UK8xbvuZ6! zYWMp()(>m9WN#oI&@n}EvO|1kr4`M*I1X-8nD7e>4?R1g9R@0#l2Y@x0kJQix8D$! zA`3SySl#cBbql-j>X02Vf4EX~KRzqbUDBTuCdj}yTL#+qldu8>^2k_H$B;q7-N-Y~J=wb>Q$p6|FS3+Q zsL~pT>vVeUfJ$4KblF=$gK&q{;30O&8*t!rZ4R7Y>fyFq0AYXV)%!CW`~p1fo{LHA zmMz)UyTo7{1=bKVqkQe8$z3)y6O&qhl~i0XN1fP-BWX`LOK-K}JoB$u&py6%3}1Eu zm9{Gq5)ZmCEZ{gkb$*}icF439*xck6Rx1p+>;CU>P`%fgSxo_*NevLUN~JV%k~LzUYvTf!P;b-|^?j%Y3U#HPY7+6Q4y3-|@T z{hw8#4<4b}9#00hgU4HSz;RFd6vfia$krTVk;hs(C~@B`Wrq+zu*XFbNpYReTkG`P z9A%Xx_?ikIys;yv+ZkdTr5axrkbV7%fU(}DJ6HeUxSM%*C-!<0S%=n#r&*ZtOwF{} zi!(GnQP}xGo!cDW&xT)Zn}f|~uCIB`GcdQcPccUsxdV4Bn^WMj|Lxx^cqt>xixrYJ zo@#3UHHUo(>fBr_>;~4{S%Ggz7%e2}Nw5&sduI-9X#$>T=>HQGFaAz!q5K(G-3C)5 zWQZt{Xu40D9t^NSFgdpC0j72qm_aKfk$u$PO^$R{aOo?9@9!#S*o$xoWv(olL(WMQ zUsGwP{F;Qp?SVkNj$)=FD;jnO^IB&*qF%YL@GEk9HxJN1odwi@sNZ#;Edg=%PaWQNiAewlx zMXu+@lV3z>GvShc0!Cr|t<5o9nYzI{G!kTa)^RU>ShZE{rX9x`-!+Ou@fj(NtK1Fo>4gx zHQ3ug)7?=CYx{=8KkCn$^*ihU3t?Ne7N=;}A1yqYSyv4f6eJ~C<8X-iCf1_kXOM-+ zF=bNq#!n80xRi{;8P*nmr-z(S@Is|VIqE=GmS{tE=CfB7jU6DIrsh&y?Ft;JZ>0Hi z940n$o@Vdgg?k;+=NA2RHED|$CFs* zy9I}T>E(2PT)EZN4NFf*puDC7RmozW)`6?E5i)yx+i27PMlzLgKP?;UXhjI}^QovC z+jhyI7*gY4R0j3-B{Lmx;y2QI0q!0F3Xu@(wWp*6O$O)8A$DG2rd%*1Om3=;)qG+; zpk!ye;l{vAnQ^c2l{!C;DBqJ%-C!)IVjx{izik<|Jn#9X;%253d-va%Mbr<8-YPcQ zM-~{?2WszziZa&xpoHg3fT+kXk2|&s*B+0NFFo=wvt8*A=<)PIHv-YN67c66e52w{ zap|IPNxx75W$lwDePp0q#*Wfy#p16{+grj>6<&@C*Aui#H~`wc z;n#qI-Z&t^s!HLSeY=rY#|GTKKFG*IF`QK7FmgrqPg#{Fkf91=Y>zv7w>!W#33VxUc!SOtbiAgfWxZ|e1Yl|lzJ96(t42Zd_4W9z zBc;CA(;9Qv$4YCfvq-6-Q6UGiwg@rsfH}HWTb=F6)ne9D^Lw)0Q6^`0vedi&j%#A@UD zQH0>4RA%K3CoaYo`kZ6hOSxbnXv>$UCvZY$e8w9KfWw7zl4FJToZErBD9eA|DhNx6R} zVf3!)tB^F*SV8qRT5!9FG4_`XmGuY*798R6MAl*lKE3su7lPXA!HCByw@SrPL$|N~R(}{{jN2 z5QOwT1*WFY=0%nOW88HRsdu_coB1i$l@R#lohw1`%QsU)s6vt5OJdI1TVmhGenqPx z`T*fyp*NTpM!-bX$t)_2emjeA%a}T;Yf2~UTSTa-5{FR*LU%EHN@i3ZRQqoO7P%Z( zNnDM-NPVbH2eO{DpZjl<{z|E^&)#20w18^!YV?Tq3QX8nTD9pqlz1cacfK_{V zy-?svfEu1VW`Mj#)u)T#NJfWZCI!SVVlT`6j3hj~Ke+(gZW*k`hn??MM!h>7PSPS3 zGD)<_oup--uA;ySsLLN(u$zCe2ICnl$CW?GJMP^Y85g6WElgGd?XmKVbSS5xDlj3d z?Amu>i*GW1$I#3Ow@vREMBUFSEZ>KqBKng1Yhz$Mk&ZaY&n09MZJPWt7W{COC&5Kl zapm;K*mJsUvzwK=g3tVu-3Xuht_eXHNNOH~3hR!N%kE~Uac+0=Z4EBoga#CJ=p@*~ z{i0_t`Uc6~Eb7N9Cc;Uq-zk)YXy#(SgYatJ`)XSrr;xu01!o<=pF(-)tjU?A(&Lo@ zhmmx@2vOGk{1UH;#QytlE1vWYAPVxBLl1A=nk7YFs4qy9C@h{E;4_dqr$=U{SxQX6 zBeyA;89FieXCAqPYxGyYb!}euD3=B{`_3NQpI=C-z28l9Hi$QgoY<~Sn4`~K8oGKKWZ)gzEj=VrRw^X;T~{nAVa|X0 zZ6g&caOTEb7{h_R&BO!}<*c@OtcMD9HF|sxG><9Jhk6{LBSY4@^U?Nm=I9Gmj%24@(tKy34dp(oet50Zt})!c_;2eNdWHKw7tKODrvB_nY0GNYS{_ym4N2FKAym$Qs&h_sv z%oiom7of)l+Ie`&SMW3HBXz^4j^9t!I$Pi`kHk)PQP*SS2X$zH9&EnQlvx}s1T(|e z4!SVZQ8=HK%KiA;?QTDwHo)vZ~szV2ikf-^%q-{l<{4MTNK{-8V zCY^N#;at+1f2>nOb^j7&`|txzM!oCYLLhz!1+q@Bv}+y>l_E%IEtWu-kn8fZ#orTq z(!IA<|GakGOBKy)gVlkUBeu8EGmCq*9Uy${mdK^_ZiqL1!FP!y!d3)c1DIqJu-uD; z5{kPFC{Xp0-Ns&IAOjf}#zgr${tE2|747cYC}2{cq-$H7afzLBVAO%o1h#A#_Mx7s z)I*;*x(^^X8%Nr;RDl5Y{Z};Xk3b|7{kk^$lK@!9-4y*_QwYG+ZD<9JqL|ga1EA#o zCU-#|G9T&5D0B?I1i@-ec~zF*8a_&ZOvLIh+nG`bLFLs)H$qB-J$?g2N&s`=i>Qb} zzh_gq}_=s=`Z+lWi|EJCEO{|eZ~Z_;nXxh_ux+E}L;mI=;!cHoi)MYpBO zcpT``03r|sZ*oE5f282y;eXGA-A4KF>})+c&?6xSSWfyr%?AqIMU#&vLWdBuLFg>}*_afGykAXP&XvrsR_H)E&34 z8XULS==2F8p;{R1=)1LRE%A3Qdq?t9N9y$lGL#fCurbNXqzHV&>2IEr(2mCL{{lgF zR8xLT&1Z@T*2X_Uf$^}y_;&J4;;uH%1dKUCnzmnYQl!uOwa0E1I-nxS%B%>2(wL2m zx4%K4MIU}Bgn*xlqEqUN%A2uNG$~G+@LjSAc=iz4J*d>|&T=%4vNTb%Yq~cu9+iL{{xsA;#EEqX%a|1 zB)<|3svdl>0s1Lo!cn6lboY^hWS5k04%z!XdJtbu+HIPweDHxyUWnQ?VHc$W^1U~y z!DoOFk}dfy?k`Ff#r>Ti3=Rdjuxs%V(eMh$A!rJMkY+ms%SBqYxVgtS7EMhJZ>0h4UGMbehX%{^_1X zKw^Yv)^U9H&)-Jus6Jm#z6sVz2_C2~m+1tsC5wLX{fj=CK&Z=^PUSr)NK@ZLna_$X zVX-i5FV?h68jSycbT`8=dtwbl$Zri~z(WeG#2W~zdyZpC1UB{fNiL{h0K@M(b~nrX zl_8hIg?lx0yOquc&~@)v%u48Lf#Tdy^?)V z$3Q1Gk_{zcvsvOWSQdRb7n`luxA>VXv0|C-0}W060QGZV7V@t+8_r9$|E$pq-xB}% z(ItiLpLv(dY!O&`E)&s{x*wXbd{+F@RAmuz(SYkaJ`Z34; z+cyVljd-~`#MaqrgzLrX#DHO_kV(&WIEyQ8>h6obw>8MHqeZEKapIdhI4kRDyEGDD zhShDHb`?xo=JQtabA`jOf3crg6isV+Ebrc#mAdMJg|He~{Ox)n@WxH?d9P zV1M&e(u?~!$7}TU0s?Ep31!sSHIS_FdjhB{bU?iI@Ju*0)ytT_$mGvIx^I6~t@qW* z{r2=+Uafh~Kim--^Nwg|V`YcAfBI-otS>3ZZ=14skj~8~iZ1IadWhaUd~q!_?yv=y zT57=>SKKdt{1sP*1;a2z#YxOR{#>f1#@Tk3cHiMmf>X#aqX!>rGImwVn1rq^f?%jk6bUJ94qQDNH~hV zrwx|sQ?EKiNj`>hZo8tNJ+@0bunCK3pzOHfz|Mq8=o8LU0>9tCf*fQ`nNxME-F#jH z;F6Ngmtl+`8~)m4`Orww^$YZYjOZxSi@#nU9PzrF}mn}Z+< zE?6TV1nx5RIk$0*+8Mte5E-eP_wby!BBNf42(W8^pe!m~#1+24(gzp6rq5_}_Cb58 z$8UWv#KRKd$I*AduryV=rDfko6jGCO96lXPV z#$-jJ7@J1Zfgy0$0+oJKutXJK@9QVwW5jlupTiE_W^E06zhHx1Uhv=9ZS+$Ao2DI0 zuQ7P>f*YJ665GoTT9U08%Yqex@enK|b+rDl7_>SOWPjvBUw!+&=U-;Bw11S*wK6ZK zgv>5<#}^(LKbcZ2WPq z&*CiFBP#BA9?UMw8fd8N^V}8axN#eeuX+Xs{H)*Z;uvk2i&)brG5rHoso2Q@!R*SM zCM{bG0~83H9qw6Zflp)22;`Y5@bF(;(H2<_*De=8zbOs+5B_$0HEmGmN67k|j&Dro z_HA9CVkh#8n{@ROeKJ>1`@LtpH@a`~h@o=$qkto73D7+xsDwau8pRiAvG^5zC|3S; zmGtpWBVW77{+wBK@b+qeHQXd%DqJuBIbmaao-0~j=e*~}9XA}sz@)M?| zl%~)@YWU4?l!^-|zu9E57|O?joL7>+Z2pC->mBZ?h7LR~PdW8Me!v5}%sxsS0%ak+ ztd8C>cGEFzZ1Ve}CcdJP^@ektV-G#si@{)<6pfNoav7`mE~<#M9hQ5IRE{3;x1Mq% zrA14PYVKVck@1@ZR{Hvx;fo(NiGnRbAxk>fRb9@f zq4ue2TPKpE^bOtP%X`|5{0~2V>eL)TaLOXBXTllje^ec1PLKWY#6pbLf)pE2FL-mK zA-97Oo_3U%R{dfdihj2E5Zzdcq*pZ5hJ%|ijJKB*PDwNKi!+)p_Mq zM(g*c9_X7uD^i{^zo_$&JEHc+((RY|wuHW0Ouk-qwUVaSC8m+u%?J{csQ1Wnd>y2u ztXNk1Rnk##%HLfzBaybpvDY+Ku)BAn!eqg9Wrv`QMl;g*fMZn0{dcX;JN?FLZgrNd zsZ(>DaNuXrzx_4qxel%2D;J?cSmv0=uB_L|w8FM2Q%fHR+u;&+I5k#)vT`+g?`ZHVUrX_b}$4 zjd7a}fNNK~=vsiF*Qe_T7hY(6FS8{&+0=YJBTQRPnQNppkiDjc5_e*ZW{R$iQF!yC z>5EHEvlc5VvgBXpLP$S+@WobKzzXMms@CJ#;#uNNtBA_#o@Bh;6=2+hJdIxbgVDJ1 zjcAItETo|OXG&dF>!R?N0h(ESgWtQsr%Mt#y1B+1?jA@^zY2PT)i}wDcW832C8Agv)d`3iBkAxTJw5L7a=}^sf^d>7@d-_aJrsj?f@G0Mmx(nHCj%0eCXM7nLax;mu>o%m8xmX zzb@XS8zroKmr4ILyJh%52hCMlGai*pr^Fbq<2EFfoovDxo!}hzmk;@>t(PmyK}%z_ z?z^z|NLx27A~pl=^C7nC=rMH_J0Q?xO(vln`%J&_WshIwG0dB5`-b(^GyS=TC$IKW zlLHhIhKv^<;^eAYyT&ypJ}E9UCm&nS{EM}zp?e!R%*B(}ywTdDuDR}mPS44|Dv`%M z!AdP~%ch5Dk6*y#v1*_6JuAU9Rd^|cJD-G+wnQ*p4=Bj|a_Xp3I zd1FD(4{N}Eo?lQysNz&aJ4DmY!)L*xIbi!aI(Y<-3$hKR@t=@CCkPWA(bL6<&AYOX z4j%p-$`+W!NZ3g3w+YRKvrlyffz4~ah*w$4MMBvjL#mU=wFI=KKE?`kEq>^gXEpJ% zk*L3Tzh~5-xiT&G?c88DlpiQXnG>IDs<^6@SxS(QQ^oMC_)vO=crACLFYf8dtwU|> zBthA4(T|P3tl67d^g}Cx?S`N(2OegRm13{67$3Gj39FH)U`8}|o4F;R(zX?+OFOJY zy27IWvM_{o!d_e9--`&IX#IX-ZMLu|U}s(M67Zth-I z3&gXzp;zfd?vIP6iuL-!c}~tu6Vq|%F7fTjvuJx#>{VBJN6h6Y9Vm z%(nAte>Xu&RyCYx5LvI=INI}L_PStbRO3H4ygbu!ZQj)BXo99s9YN9v@rzam;H+Jz zk)R5iA8Y6+g|mm;q8NcdLHn6{&ACo6L#Ml2d3{+qFeSUzf~mz1nyZoPX5!uI(Uhxu zR>^SaN?mc)``tE>S|qFie6&e&JBsJiiIa2>(ay}_?rdy5naM;fPbMexJ}yeIXC%JM z<`i2B7Vz~MsY<3Bfvr8w)uiFbRyf}h0grT#QANb-IOohl%*`tY%{7!NSsmLJj$ga^ z;^P}p12P?pP}>dk?)i&k^O}sN{ISzpzeV%be(!dy3$~nor(_Z5g*+#u$zWddV)h+> zj_G7DxK)_r?<3{BX4>r@J1p*BPm$6Z!P| z5par5j6jSfGLBW5pFzI(OiLWG<+01(7O0JPT>NC|_4BzkZyC=viN&WCPC{8s4%6?J zDgCh+lj^GfxsKx{2M;Kp>deazDGS3s=gmYnu)P4L=$YGcLHoBpcv|L)KwC%uDa-gn zWm69(_51P(o!XOYEgmedy~+XuztzsKep_~CegSm%T#MGd=^gt6AY85a#@D;TMe^Im zU(r4fS|fh0=t);!*_M$59YTrC}>yl2r*2yuaoi{d`P- z%V(;3lz}wN4+sA1J-Z19SiNLih=9}7>EiR3JQaghd`9{qL6WZe{F5v`5Ae2d9}>;j zNlkh_*GAND-5ctD13Rr}+pI687Ny;PIRDPve>Tyg{ELI7t5W4Na=h`*CF4R($Gk|C zxf9F76=VzU!&0&>6d?g~iCvh8A(ad!T+6YdXNY`6%dkDu#w2S`gOQ!X7=WNjWq$93 zndF19_;~Oi#T3xA1<$|oV6YDO7xk|w>9=c*rcCMnqb!$U%@wC=hye?n)Q|n0284OX<+}FCC54zHEptZw;ZC& z?j^b##pe>cmGMwj1v-BizdSb~(0s}qcHHZu$}~6YD3m; z)7tW5<P=Q$G0ma_89}y=Jx}0G<(Fv-5%ka3>L?d!kTA)igtE#8sApu3Y#hX zH!2I1e4trcErUor-uAJ%I?IbX9(oV7WXh6DkaWoM`1NaRP1YZ?jiY{BG79jf=`Wr~ z|J#@1l;BuFUslM|R}UZEpE^K3AGzu5TOU1op!5f~d;hD|0Qu+R$d7AGZruUT!HL32 zPBTj}RzP153#Cu8#WrJ)0DkKQHMti7pZ%wl``lmTFN$jZI#C^Puj76A_OLf2Hd)T3 zQ0C9;f*<5OJFm=?Y#iBn|Gw*oIiHyOmNfL?o!)0R)zeSyvxrU|f_l5%QQ0QjORe|i zQGHTAI0!pxZQm2hX4V4HA$c0L@n-7jN7j})%f5xDYaOp|&-9fY>Yk;BjsXP+ff3pF zIyJ?Qgm)vrvfHh1{4M{i;r})&EX++SdPf`j_>tb_t9q-6a-$=Lfyv-V>+>;*J+}Iv zs`rIwq1rari}5|r6fc6Xqrq>*DFTzr9Y^wg%VGD&E^h{%lQufv3^9y%AuryGtj2Ea zZb3uqR=zi16|ZxT6&Pcy^uA&i;<-UiHe$G~WT#clC{(&d-zeBP-L{B#WUs9|{?s=ZNJ2{hn)zVgpj~Gb6 zYrm8^uC>VfH9L^K)kgYp?XfH)ZOa49?^)XW&C~z!%a`sQtfpstuU&o}XLoaQ$K?se wa?+2^_?CKr;a(MZK?ieJ|KA^~JtCz2PiXhL{xu`;H-LbLXzhC?cdUc|4{+~CRsaA1 literal 0 HcmV?d00001 diff --git a/packages/extension/src/services/option/defaultSettings.test.ts b/packages/extension/src/services/option/defaultSettings.test.ts index 8f99ddc2..afbfab60 100644 --- a/packages/extension/src/services/option/defaultSettings.test.ts +++ b/packages/extension/src/services/option/defaultSettings.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest" -import { DefaultCommands, getDefaultCommands } from "./defaultSettings" +import { + DefaultCommands, + getDefaultCommands, + COMMAND_SEARCH_ID, +} from "./defaultSettings" import { isLinkCommand } from "@/lib/utils" import { INSERT, toInsertTemplate } from "@/services/pageAction" import { getAiServicesFallback } from "@/services/aiPrompt" @@ -279,4 +283,17 @@ describe("getDefaultCommands", () => { } } }) + + it("DS-24: all locale command sets should include a Search Commands on Hub command", () => { + for (const locale of ALL_LOCALES) { + const commands = getDefaultCommands(locale) + const cmd = commands.find((c) => c.id === COMMAND_SEARCH_ID) + expect( + cmd, + `Missing command search command for locale: ${locale}`, + ).toBeDefined() + expect((cmd as any).openMode).toBe("sidePanel") + expect((cmd as any).searchUrl).toContain("%pageUrl") + } + }) }) diff --git a/packages/extension/src/services/option/defaultSettings.ts b/packages/extension/src/services/option/defaultSettings.ts index 767b32a2..afa15d2a 100644 --- a/packages/extension/src/services/option/defaultSettings.ts +++ b/packages/extension/src/services/option/defaultSettings.ts @@ -80,6 +80,10 @@ const FOLDER_AI = "e4994c63-cfa7-4e49-9dfe-a79e6120a1ae" const FOLDER_MEDIA = "a3495269-0a4d-4866-a519-bca75ed1c246" const FOLDER_WORK = "01710cf1-ec8b-497f-8d1f-9cb716567bc4" +// Fixed ID for the "Search Commands on Hub" default command, used by the +// settings migration to detect whether a user already has it. +export const COMMAND_SEARCH_ID = "019f470a-cea5-7d6f-86cf-e7df9fb14ff1" + export default { settingVersion: VERSION, popupPlacement: PopupPlacement, @@ -383,8 +387,82 @@ export const DefaultCommands = [ height: PopupOption.height, }, }, + createCommandSearchCommand("Search Commands on Hub", "en"), ] as Command[] +// Creates a "Search Commands on Hub" command for the given locale. Every +// variant shares COMMAND_SEARCH_ID so the settings migration can detect +// whether a user already has one, regardless of which locale it was added in. +function createCommandSearchCommand(title: string, hubLocale: string): Command { + const iconUrl = chrome.runtime.getURL("images/search_command.png") + return { + id: COMMAND_SEARCH_ID, + title, + iconUrl, + openMode: OPEN_MODE.POPUP, + openModeSecondary: OPEN_MODE.TAB, + searchUrl: `https://selection-command.com/${hubLocale}?q=%pageUrl`, + popupOption: { + width: PopupOption.width, + height: PopupOption.height, + }, + spaceEncoding: SPACE_ENCODING.PLUS, + } as Command +} + +const CMD_COMMAND_SEARCH_JA = createCommandSearchCommand( + "コマンドをHubで検索", + "ja", +) +const CMD_COMMAND_SEARCH_ZH = createCommandSearchCommand( + "在 Hub 中搜索命令", + "zh-CN", +) +const CMD_COMMAND_SEARCH_KO = createCommandSearchCommand( + "Hub에서 명령어 검색", + "ko", +) +const CMD_COMMAND_SEARCH_RU = createCommandSearchCommand( + "Поиск команд в Hub", + "ru", +) +const CMD_COMMAND_SEARCH_DE = createCommandSearchCommand( + "Befehle im Hub suchen", + "de", +) +const CMD_COMMAND_SEARCH_FR = createCommandSearchCommand( + "Rechercher des commandes sur Hub", + "fr", +) +const CMD_COMMAND_SEARCH_ES = createCommandSearchCommand( + "Buscar comandos en Hub", + "es", +) +const CMD_COMMAND_SEARCH_PT_BR = createCommandSearchCommand( + "Pesquisar comandos no Hub", + "pt-BR", +) +const CMD_COMMAND_SEARCH_PT = createCommandSearchCommand( + "Pesquisar comandos no Hub", + "pt-PT", +) +const CMD_COMMAND_SEARCH_HI = createCommandSearchCommand( + "Hub पर कमांड खोजें", + "hi", +) +const CMD_COMMAND_SEARCH_ID = createCommandSearchCommand( + "Cari Perintah di Hub", + "id", +) +const CMD_COMMAND_SEARCH_MS = createCommandSearchCommand( + "Cari Perintah di Hub", + "ms", +) +const CMD_COMMAND_SEARCH_IT = createCommandSearchCommand( + "Cerca comandi su Hub", + "it", +) + // Common commands shared across locales const CMD_LINK_PREVIEW = { id: "$$drag-1", @@ -1744,6 +1822,7 @@ export const LOCALE_COMMANDS = { CMD_TRANSLATE_JA, CMD_DRIVE, CMD_EN_TO_JA, + CMD_COMMAND_SEARCH_JA, ], zh: [ CMD_LINK_PREVIEW, @@ -1759,6 +1838,7 @@ export const LOCALE_COMMANDS = { CMD_DRIVE, CMD_ZHIHU, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_ZH, ], ko: [ CMD_LINK_PREVIEW, @@ -1774,6 +1854,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_KO, ], ru: [ CMD_LINK_PREVIEW, @@ -1790,6 +1871,7 @@ export const LOCALE_COMMANDS = { CMD_VK, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_RU, ], de: [ CMD_LINK_PREVIEW, @@ -1805,6 +1887,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_DE, ], fr: [ CMD_LINK_PREVIEW, @@ -1820,6 +1903,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_FR, ], es: [ CMD_LINK_PREVIEW, @@ -1837,6 +1921,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_ES, ], "pt-br": [ CMD_LINK_PREVIEW, @@ -1852,6 +1937,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_PT_BR, ], pt: [ CMD_LINK_PREVIEW, @@ -1867,6 +1953,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_PT, ], hi: [ CMD_LINK_PREVIEW, @@ -1882,6 +1969,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_HI, ], id: [ CMD_LINK_PREVIEW, @@ -1897,6 +1985,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_ID, ], ms: [ CMD_LINK_PREVIEW, @@ -1912,6 +2001,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_MS, ], it: [ CMD_LINK_PREVIEW, @@ -1928,6 +2018,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, + CMD_COMMAND_SEARCH_IT, ], } as Record diff --git a/packages/extension/src/services/settings/settings.test.ts b/packages/extension/src/services/settings/settings.test.ts index 34adfff0..82c58a42 100644 --- a/packages/extension/src/services/settings/settings.test.ts +++ b/packages/extension/src/services/settings/settings.test.ts @@ -5,6 +5,7 @@ import { OptionSettings } from "../option/optionSettings" import DefaultSettings, { DefaultCommands, getDefaultCommands, + COMMAND_SEARCH_ID, } from "../option/defaultSettings" import { toDataURL } from "../dom" import { OPTION_FOLDER, VERSION, OPEN_MODE } from "@/const" @@ -670,4 +671,40 @@ describe("migrate function", () => { expect(result.windowOption).toBeDefined() expect(result.windowOption.sidePanelAutoHide).toBe(true) // Should preserve existing value }) + + it("ST-34: should add Search Commands on Hub command if not exists", async () => { + const oldData = { + settingVersion: "1.1.0", + commands: [], + folders: [], + pageRules: [], + } as any + + const result = await migrate(oldData) + + const cmd = result.commands.find((c: Command) => c.id === COMMAND_SEARCH_ID) + expect(cmd).toBeDefined() + expect(mockStorage.setCommands).toHaveBeenCalled() + }) + + it("ST-34-a: should not duplicate Search Commands on Hub command if already exists", async () => { + const existing = { + id: COMMAND_SEARCH_ID, + title: "Search Commands on Hub", + } + const oldData = { + settingVersion: "1.1.0", + commands: [existing], + folders: [], + pageRules: [], + } as any + + const result = await migrate(oldData) + + const matches = result.commands.filter( + (c: Command) => c.id === COMMAND_SEARCH_ID, + ) + expect(matches).toHaveLength(1) + expect(matches[0]).toBe(existing) + }) }) diff --git a/packages/extension/src/services/settings/settings.ts b/packages/extension/src/services/settings/settings.ts index eb7afdc3..a31d746b 100644 --- a/packages/extension/src/services/settings/settings.ts +++ b/packages/extension/src/services/settings/settings.ts @@ -4,6 +4,7 @@ import DefaultSettings, { DefaultCommands, getDefaultCommands, PopupPlacement, + COMMAND_SEARCH_ID, } from "../option/defaultSettings" import { OPTION_FOLDER, @@ -262,6 +263,9 @@ export const migrate = async (data: SettingsType): Promise => { if (versionDiff(currentVersion, "0.15.1") === VersionDiff.Old) { data = migrate0_15_1(data) } + if (versionDiff(currentVersion, "1.1.0") === VersionDiff.Old) { + data = await migrate1_1_0(data) + } data.settingVersion = VERSION as Version return data @@ -381,3 +385,19 @@ const migrate0_15_1 = (data: SettingsType): SettingsType => { } return data } + +const migrate1_1_0 = async (data: SettingsType): Promise => { + // Add the "Search Commands on Hub" command if not exists. + const hasCommandSearch = data.commands.some((c) => c.id === COMMAND_SEARCH_ID) + if (!hasCommandSearch) { + const defaultCommand = getDefaultCommands(getUILanguage()).find( + (c) => c.id === COMMAND_SEARCH_ID, + ) + if (defaultCommand != null) { + data.commands.push(defaultCommand) + await Storage.setCommands(data.commands) + console.debug("migrate 1.1.0: added command search") + } + } + return data +} From 32ac75e7855d7e2aec26d786d3cc847fa9a7a11a Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Fri, 10 Jul 2026 12:47:37 +0900 Subject: [PATCH 04/10] Update: fix review comments. --- .../extension/src/services/option/defaultSettings.test.ts | 2 +- packages/extension/src/services/option/defaultSettings.ts | 4 ++-- packages/extension/src/services/settings/settings.test.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/extension/src/services/option/defaultSettings.test.ts b/packages/extension/src/services/option/defaultSettings.test.ts index afbfab60..afc48198 100644 --- a/packages/extension/src/services/option/defaultSettings.test.ts +++ b/packages/extension/src/services/option/defaultSettings.test.ts @@ -292,7 +292,7 @@ describe("getDefaultCommands", () => { cmd, `Missing command search command for locale: ${locale}`, ).toBeDefined() - expect((cmd as any).openMode).toBe("sidePanel") + expect((cmd as any).openMode).toBe("popup") expect((cmd as any).searchUrl).toContain("%pageUrl") } }) diff --git a/packages/extension/src/services/option/defaultSettings.ts b/packages/extension/src/services/option/defaultSettings.ts index afa15d2a..96e0f0df 100644 --- a/packages/extension/src/services/option/defaultSettings.ts +++ b/packages/extension/src/services/option/defaultSettings.ts @@ -450,7 +450,7 @@ const CMD_COMMAND_SEARCH_HI = createCommandSearchCommand( "Hub पर कमांड खोजें", "hi", ) -const CMD_COMMAND_SEARCH_ID = createCommandSearchCommand( +const CMD_COMMAND_SEARCH_ID_LANG = createCommandSearchCommand( "Cari Perintah di Hub", "id", ) @@ -1985,7 +1985,7 @@ export const LOCALE_COMMANDS = { CMD_NETFLIX, CMD_DRIVE, CMD_CHARACTER_COUNTER, - CMD_COMMAND_SEARCH_ID, + CMD_COMMAND_SEARCH_ID_LANG, ], ms: [ CMD_LINK_PREVIEW, diff --git a/packages/extension/src/services/settings/settings.test.ts b/packages/extension/src/services/settings/settings.test.ts index 82c58a42..4e08af6a 100644 --- a/packages/extension/src/services/settings/settings.test.ts +++ b/packages/extension/src/services/settings/settings.test.ts @@ -674,7 +674,7 @@ describe("migrate function", () => { it("ST-34: should add Search Commands on Hub command if not exists", async () => { const oldData = { - settingVersion: "1.1.0", + settingVersion: "1.0.0", commands: [], folders: [], pageRules: [], @@ -693,7 +693,7 @@ describe("migrate function", () => { title: "Search Commands on Hub", } const oldData = { - settingVersion: "1.1.0", + settingVersion: "1.0.0", commands: [existing], folders: [], pageRules: [], From 4acc340323c13db8b96c27a57cd2f97353413d11 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Fri, 10 Jul 2026 12:59:46 +0900 Subject: [PATCH 05/10] Fix: use tab URL instead of location.href for pageUrl in background execution path location.href inside action modules (tab/popup/window/backgroundTab/pageAction/aiPrompt) resolved to the service worker's own URL, not the visited page, when invoked via the shortcut-key fallback path in background_script.ts. Thread pageUrl explicitly through ExecuteCommandParams instead: content script callers supply location.href, and the background fallback supplies the already-queried tab.url. Co-Authored-By: Claude Sonnet 5 --- packages/extension/src/action/aiPrompt.test.ts | 7 +------ packages/extension/src/action/aiPrompt.ts | 7 ++++--- packages/extension/src/action/backgroundTab.ts | 3 ++- packages/extension/src/action/executor.ts | 2 ++ packages/extension/src/action/index.ts | 3 +++ packages/extension/src/action/pageAction.ts | 5 +++-- packages/extension/src/action/popup.ts | 3 ++- packages/extension/src/action/tab.ts | 3 ++- packages/extension/src/action/window.ts | 3 ++- packages/extension/src/background_script.test.ts | 1 + packages/extension/src/background_script.ts | 1 + packages/extension/src/types/index.ts | 1 + 12 files changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/extension/src/action/aiPrompt.test.ts b/packages/extension/src/action/aiPrompt.test.ts index b067112c..7d57f7ce 100644 --- a/packages/extension/src/action/aiPrompt.test.ts +++ b/packages/extension/src/action/aiPrompt.test.ts @@ -106,11 +106,6 @@ const baseCommand = { describe("AiPrompt.execute", () => { beforeEach(() => { vi.clearAllMocks() - // Mock location.href for content script context - Object.defineProperty(window, "location", { - value: { href: "https://example.com/page" }, - writable: true, - }) }) describe("DOM input approach (no queryUrl)", () => { @@ -565,10 +560,10 @@ describe("AiPrompt.execute", () => { }, } as any, position: { x: 0, y: 0 }, + pageUrl: "https://example.com/page", }) const sentArgs = vi.mocked(Ipc.send).mock.calls[0][1] as any - // location.href is "https://example.com/page" from beforeEach mock expect(sentArgs.url.selectionText).toBe( "Summarize [https://example.com/page](https://example.com/page)", ) diff --git a/packages/extension/src/action/aiPrompt.ts b/packages/extension/src/action/aiPrompt.ts index 8f986079..af0a762f 100644 --- a/packages/extension/src/action/aiPrompt.ts +++ b/packages/extension/src/action/aiPrompt.ts @@ -69,6 +69,7 @@ export const AiPrompt = { position, useSecondary, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isAiPromptType(command)) { console.error("command is not for AiPrompt.") @@ -128,7 +129,7 @@ export const AiPrompt = { // is false and the DOM input approach is used instead. const expandedPrompt = safeInterpolate(aiPromptOption.prompt, { [InsertSymbol[INSERT.SELECTED_TEXT]]: selectionText, - [InsertSymbol[INSERT.URL]]: location.href, + [InsertSymbol[INSERT.URL]]: pageUrl ?? "", [InsertSymbol[INSERT.LANG]]: getUILanguage(), }) @@ -359,7 +360,7 @@ export const AiPrompt = { url: serviceUrl, steps, selectedText: selectionText, - srcUrl: location.href, + srcUrl: pageUrl ?? "", clipboardText: "", useClipboard: !useQueryUrl && (needClipboard || (useClipboard ?? false)), @@ -407,7 +408,7 @@ export const AiPrompt = { height: command.popupOption?.height ?? PopupOption.height, width: command.popupOption?.width ?? PopupOption.width, selectedText: selectionText, - srcUrl: location.href, + srcUrl: pageUrl ?? "", openMode, pageHtml, selectionHtml, diff --git a/packages/extension/src/action/backgroundTab.ts b/packages/extension/src/action/backgroundTab.ts index 8ddd70df..d01900c2 100644 --- a/packages/extension/src/action/backgroundTab.ts +++ b/packages/extension/src/action/backgroundTab.ts @@ -9,6 +9,7 @@ export const BackgroundTab = { selectionText, command, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isValidString(command.searchUrl)) { console.error("searchUrl is not valid.") @@ -21,7 +22,7 @@ export const BackgroundTab = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, - pageUrl: location.href, + pageUrl: pageUrl ?? "", }, active: false, }) diff --git a/packages/extension/src/action/executor.ts b/packages/extension/src/action/executor.ts index bf6651ce..52fcddac 100644 --- a/packages/extension/src/action/executor.ts +++ b/packages/extension/src/action/executor.ts @@ -12,6 +12,7 @@ export async function executeAction({ useSecondary = false, useClipboard = false, changeState, + pageUrl, }: ExecuteCommandParams & { actions: Record }) { let mode = command.openMode as OPEN_MODE if ( @@ -30,6 +31,7 @@ export async function executeAction({ useClipboard, changeState: changeState ?? (() => {}), target: target ?? null, + pageUrl, }) const { sourceType, sourceId } = resolveCommandSource(command) diff --git a/packages/extension/src/action/index.ts b/packages/extension/src/action/index.ts index 2ecfb215..ddf710ef 100644 --- a/packages/extension/src/action/index.ts +++ b/packages/extension/src/action/index.ts @@ -48,6 +48,9 @@ export async function execute({ useSecondary, useClipboard, changeState, + // This module only ever runs in the content script's page context, so + // location.href here correctly reflects the visited page. + pageUrl: location.href, actions, }) } diff --git a/packages/extension/src/action/pageAction.ts b/packages/extension/src/action/pageAction.ts index dd1788ca..9c92ec15 100644 --- a/packages/extension/src/action/pageAction.ts +++ b/packages/extension/src/action/pageAction.ts @@ -19,6 +19,7 @@ export const PageAction = { useSecondary, useClipboard, userVariables, + pageUrl, }: ExecuteCommandParams & PageActionParams) { if (!isPageActionCommand(command)) { console.error("command is not for PageAction.") @@ -46,7 +47,7 @@ export const PageAction = { searchUrl: command.pageActionOption.startUrl, selectionText, useClipboard: needClipboard || (useClipboard ?? false), - pageUrl: location.href, + pageUrl: pageUrl ?? "", } const openMode = useSecondary @@ -69,7 +70,7 @@ export const PageAction = { height: command.popupOption?.height ?? PopupOption.height, width: command.popupOption?.width ?? PopupOption.width, selectedText: selectionText, - srcUrl: location.href, + srcUrl: pageUrl ?? "", openMode, userVariables: userVariables ?? command.pageActionOption.userVariables, }) diff --git a/packages/extension/src/action/popup.ts b/packages/extension/src/action/popup.ts index 04062274..9e5ddc5e 100644 --- a/packages/extension/src/action/popup.ts +++ b/packages/extension/src/action/popup.ts @@ -12,6 +12,7 @@ export const Popup = { command, position, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isValidString(command.searchUrl)) { console.error("searchUrl is not valid.") @@ -31,7 +32,7 @@ export const Popup = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, - pageUrl: location.href, + pageUrl: pageUrl ?? "", }, top: Math.floor(windowPosition.top + position.y), left: Math.floor(windowPosition.left + position.x), diff --git a/packages/extension/src/action/tab.ts b/packages/extension/src/action/tab.ts index 94e6eb1d..be36d062 100644 --- a/packages/extension/src/action/tab.ts +++ b/packages/extension/src/action/tab.ts @@ -9,6 +9,7 @@ export const Tab = { selectionText, command, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isValidString(command.searchUrl)) { console.error("searchUrl is not valid.") @@ -21,7 +22,7 @@ export const Tab = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, - pageUrl: location.href, + pageUrl: pageUrl ?? "", }, active: true, }) diff --git a/packages/extension/src/action/window.ts b/packages/extension/src/action/window.ts index 1881a42d..527302ec 100644 --- a/packages/extension/src/action/window.ts +++ b/packages/extension/src/action/window.ts @@ -12,6 +12,7 @@ export const Window = { command, position, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isValidString(command.searchUrl)) { console.error("searchUrl is not valid.") @@ -31,7 +32,7 @@ export const Window = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, - pageUrl: location.href, + pageUrl: pageUrl ?? "", }, top: Math.floor(windowPosition.top + position.y), left: Math.floor(windowPosition.left + position.x), diff --git a/packages/extension/src/background_script.test.ts b/packages/extension/src/background_script.test.ts index 8bce14bb..587632e5 100644 --- a/packages/extension/src/background_script.test.ts +++ b/packages/extension/src/background_script.test.ts @@ -353,6 +353,7 @@ describe("Background Script Migration", () => { selectionText: "test selection text", target: null, useClipboard: false, + pageUrl: "https://example.com", }), ) }) diff --git a/packages/extension/src/background_script.ts b/packages/extension/src/background_script.ts index d246ac10..83b7108c 100644 --- a/packages/extension/src/background_script.ts +++ b/packages/extension/src/background_script.ts @@ -585,6 +585,7 @@ chrome.commands.onCommand.addListener(async (commandName) => { selectionText, target: null, useClipboard, + pageUrl: tab?.url ?? "", }) } sendEvent( diff --git a/packages/extension/src/types/index.ts b/packages/extension/src/types/index.ts index 5b357c44..2fa75be7 100644 --- a/packages/extension/src/types/index.ts +++ b/packages/extension/src/types/index.ts @@ -238,6 +238,7 @@ export type ExecuteCommandParams = { useSecondary?: boolean useClipboard?: boolean changeState?: (state: ExecState, message?: string) => void + pageUrl?: string } export type ActionTypes = PAGE_ACTION_EVENT | PAGE_ACTION_CONTROL From 948b7435f0a3ace841f8917be6e5cce272e84801 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Fri, 10 Jul 2026 23:15:51 +0900 Subject: [PATCH 06/10] Fix: determine isTextSelectionOnly per command type using schema type guards Previously only aiPrompt checked for selectedText/clipboard placeholders, so search commands with a fixed URL were incorrectly flagged in SidePanel mode, and pageAction steps using {{SelectedText}} were never flagged even though selection can't be substituted by the clipboard fallback. Co-Authored-By: Claude Sonnet 5 --- .../components/option/editor/ShortcutList.tsx | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/extension/src/components/option/editor/ShortcutList.tsx b/packages/extension/src/components/option/editor/ShortcutList.tsx index 2fef9bb0..0d4436f0 100644 --- a/packages/extension/src/components/option/editor/ShortcutList.tsx +++ b/packages/extension/src/components/option/editor/ShortcutList.tsx @@ -19,8 +19,9 @@ import { } from "@/services/option/commandTree" import { cn } from "@/lib/utils" import css from "./ShortcutList.module.css" -import { isAiPromptType } from "@/types/schema" +import { isAiPromptType, isSearchType, isPageActionType } from "@/types/schema" import { INSERT, toInsertTemplate } from "@/services/pageAction" +import { paramToStr } from "@/services/pageAction/helper" const t = (key: string, p?: string[]) => _t(`Option_${key}`, p) @@ -28,14 +29,33 @@ type ShortcutListProps = { control: any } +// Checks whether the given text references the selected text or clipboard placeholder. +const hasSelectionOrClipboardPlaceholder = (text: string) => + text.includes(toInsertTemplate(INSERT.SELECTED_TEXT)) || + text.includes(toInsertTemplate(INSERT.CLIPBOARD)) + const isTextSelectionOnly = (command: Command) => { const { openMode } = command + + if (isSearchType(command)) { + // Clipboard access doesn't work in SidePanel mode, so a selection is + // required only when the searchUrl actually needs text (%s). + return ( + openMode === OPEN_MODE.SIDE_PANEL && + (command.searchUrl?.includes("%s") ?? false) + ) + } + + if (isPageActionType(command)) { + // A selection is required if any step references selected text or clipboard. + return command.pageActionOption.steps.some((step) => + hasSelectionOrClipboardPlaceholder(paramToStr(step.param)), + ) + } + if (isAiPromptType(command)) { const option = command.aiPromptOption - - const willUseClipboard = - option.prompt.includes(toInsertTemplate(INSERT.CLIPBOARD)) || - option.prompt.includes(toInsertTemplate(INSERT.SELECTED_TEXT)) + const willUseClipboard = hasSelectionOrClipboardPlaceholder(option.prompt) return option.openMode === OPEN_MODE.SIDE_PANEL && willUseClipboard } From 75302e54e2113d73ce07c3270abf00b46354b5c8 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Fri, 10 Jul 2026 23:24:12 +0900 Subject: [PATCH 07/10] Fix: only show no-selection-behavior field for commands that reference selection showNoSel was based solely on !isTextSelectionOnly, so commands that don't reference selected text or clipboard at all (e.g. a fixed searchUrl with no %s) still showed the dropdown, even though the setting has no effect on them. Co-Authored-By: Claude Sonnet 5 --- .../components/option/editor/ShortcutList.tsx | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/extension/src/components/option/editor/ShortcutList.tsx b/packages/extension/src/components/option/editor/ShortcutList.tsx index 0d4436f0..7adefddc 100644 --- a/packages/extension/src/components/option/editor/ShortcutList.tsx +++ b/packages/extension/src/components/option/editor/ShortcutList.tsx @@ -62,6 +62,28 @@ const isTextSelectionOnly = (command: Command) => { return !Object.values(OPEN_MODE_BG).includes(openMode as any) } +// Checks whether the command references the selected text or clipboard at +// all, regardless of openMode. Used to decide whether the "no selection +// behavior" setting has any effect on the command. +const referencesSelection = (command: Command): boolean => { + if (isSearchType(command) || command.openMode === OPEN_MODE.API) { + return command.searchUrl?.includes("%s") ?? false + } + + if (isPageActionType(command)) { + return command.pageActionOption.steps.some((step) => + hasSelectionOrClipboardPlaceholder(paramToStr(step.param)), + ) + } + + if (isAiPromptType(command)) { + return hasSelectionOrClipboardPlaceholder(command.aiPromptOption.prompt) + } + + // copy / getTextStyles / linkPopup always operate on the current selection. + return true +} + const createNameRender = (command: Command) => { return isTextSelectionOnly(command) ? (name: string) => ( @@ -265,7 +287,10 @@ export function ShortcutList({ control }: ShortcutListProps) { const selectedCmd = userCommands.find( (c: Command) => c?.id === targetId, ) - const showNoSel = selectedCmd && !isTextSelectionOnly(selectedCmd) + const showNoSel = + selectedCmd && + !isTextSelectionOnly(selectedCmd) && + referencesSelection(selectedCmd) return (
From 51c0abe5b0ca6c068523aa46a7b143a7d272f2df Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Sat, 11 Jul 2026 21:14:17 +0900 Subject: [PATCH 08/10] Refactor: share placeholder-based checks between isTextSelectionOnly and showNoSel logic isTextSelectionOnly now delegates its clipboard/selectedText placeholder detection to willUseClipboard, and gates the pageAction branch on PAGE_ACTION_OPEN_MODE.CURRENT_TAB. The no-selection-behavior field visibility now uses a dedicated referencesSelection helper that checks for the selectedText placeholder specifically. Co-Authored-By: Claude Sonnet 5 --- .../components/option/editor/ShortcutList.tsx | 83 ++++++++++++------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/packages/extension/src/components/option/editor/ShortcutList.tsx b/packages/extension/src/components/option/editor/ShortcutList.tsx index 7adefddc..8d069153 100644 --- a/packages/extension/src/components/option/editor/ShortcutList.tsx +++ b/packages/extension/src/components/option/editor/ShortcutList.tsx @@ -9,6 +9,7 @@ import type { Command, CommandFolder, ShortcutCommand } from "@/types" import { OPEN_MODE, OPEN_MODE_BG, + PAGE_ACTION_OPEN_MODE, SHORTCUT_PLACEHOLDER, SHORTCUT_NO_SELECTION_BEHAVIOR, } from "@/const" @@ -34,37 +35,57 @@ const hasSelectionOrClipboardPlaceholder = (text: string) => text.includes(toInsertTemplate(INSERT.SELECTED_TEXT)) || text.includes(toInsertTemplate(INSERT.CLIPBOARD)) +// Checks whether the command references the selected text or clipboard at +// all, regardless of openMode. Used to decide whether the "no selection +// behavior" setting has any effect on the command. +const willUseClipboard = (command: Command): boolean => { + if (isSearchType(command) || command.openMode === OPEN_MODE.API) { + return command.searchUrl?.includes("%s") ?? false + } + + if (isPageActionType(command)) { + return command.pageActionOption.steps.some((step) => + hasSelectionOrClipboardPlaceholder(paramToStr(step.param)), + ) + } + + if (isAiPromptType(command)) { + return hasSelectionOrClipboardPlaceholder(command.aiPromptOption.prompt) + } + + // copy / getTextStyles / linkPopup always operate on the current selection. + return true +} + const isTextSelectionOnly = (command: Command) => { const { openMode } = command if (isSearchType(command)) { // Clipboard access doesn't work in SidePanel mode, so a selection is - // required only when the searchUrl actually needs text (%s). - return ( - openMode === OPEN_MODE.SIDE_PANEL && - (command.searchUrl?.includes("%s") ?? false) - ) + // required only when the command actually needs text (%s). + return openMode === OPEN_MODE.SIDE_PANEL && willUseClipboard(command) } if (isPageActionType(command)) { - // A selection is required if any step references selected text or clipboard. - return command.pageActionOption.steps.some((step) => - hasSelectionOrClipboardPlaceholder(paramToStr(step.param)), + return ( + command.pageActionOption.openMode === PAGE_ACTION_OPEN_MODE.CURRENT_TAB && + willUseClipboard(command) ) } if (isAiPromptType(command)) { - const option = command.aiPromptOption - const willUseClipboard = hasSelectionOrClipboardPlaceholder(option.prompt) - return option.openMode === OPEN_MODE.SIDE_PANEL && willUseClipboard + return ( + command.aiPromptOption.openMode === OPEN_MODE.SIDE_PANEL && + willUseClipboard(command) + ) } return !Object.values(OPEN_MODE_BG).includes(openMode as any) } -// Checks whether the command references the selected text or clipboard at -// all, regardless of openMode. Used to decide whether the "no selection -// behavior" setting has any effect on the command. +const hasSelectionPlaceholder = (text: string) => + text.includes(toInsertTemplate(INSERT.SELECTED_TEXT)) + const referencesSelection = (command: Command): boolean => { if (isSearchType(command) || command.openMode === OPEN_MODE.API) { return command.searchUrl?.includes("%s") ?? false @@ -72,12 +93,12 @@ const referencesSelection = (command: Command): boolean => { if (isPageActionType(command)) { return command.pageActionOption.steps.some((step) => - hasSelectionOrClipboardPlaceholder(paramToStr(step.param)), + hasSelectionPlaceholder(paramToStr(step.param)), ) } if (isAiPromptType(command)) { - return hasSelectionOrClipboardPlaceholder(command.aiPromptOption.prompt) + return hasSelectionPlaceholder(command.aiPromptOption.prompt) } // copy / getTextStyles / linkPopup always operate on the current selection. @@ -87,18 +108,18 @@ const referencesSelection = (command: Command): boolean => { const createNameRender = (command: Command) => { return isTextSelectionOnly(command) ? (name: string) => ( - - {name} - - {t("shortcut_text_selection_only")} - + + {name} + + {t("shortcut_text_selection_only")} - ) + + ) : undefined } @@ -168,6 +189,8 @@ export function ShortcutList({ control }: ShortcutListProps) { name: "shortcuts.shortcuts", }) + const isShortcutsEmpty = fields.length === 0 + useEffect(() => { if (!shortcutValues) return shortcutValues.forEach((shortcut: ShortcutCommand, index: number) => { @@ -178,7 +201,7 @@ export function ShortcutList({ control }: ShortcutListProps) { cmd && isTextSelectionOnly(cmd) && shortcut?.noSelectionBehavior !== - SHORTCUT_NO_SELECTION_BEHAVIOR.DO_NOTHING + SHORTCUT_NO_SELECTION_BEHAVIOR.DO_NOTHING ) { setValue( `shortcuts.shortcuts.${index}.noSelectionBehavior`, @@ -221,14 +244,14 @@ export function ShortcutList({ control }: ShortcutListProps) { useEffect(() => { // Initialize the shortcuts - if (fields.length !== 0) return + if (!isShortcutsEmpty) return const initialData = commands.map((cmd) => ({ id: cmd.name || "", commandId: SHORTCUT_PLACEHOLDER, noSelectionBehavior: SHORTCUT_NO_SELECTION_BEHAVIOR.USE_CLIPBOARD, })) replace(initialData) - }, [replace, commands, userCommands]) + }, [isShortcutsEmpty, replace, commands, userCommands]) const options = useMemo( () => flattenCommandsAndFolders(userCommands, folders), From 1e8e803e8e65ee313beeb57366124ab055ebe623 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Sat, 11 Jul 2026 23:15:21 +0900 Subject: [PATCH 09/10] Fix: guard chrome.runtime.getURL in createCommandSearchCommand for non-extension build contexts yarn build:e2e runs generate-e2e-urls.ts via vite-node, which imports defaultSettings.ts outside a Chrome extension runtime, causing ReferenceError: chrome is not defined when constructing the "Search Commands on Hub" default command. Co-Authored-By: Claude Sonnet 5 --- packages/extension/e2e/generated-command-urls.ts | 14 ++++++++++++++ .../src/services/option/defaultSettings.ts | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/extension/e2e/generated-command-urls.ts b/packages/extension/e2e/generated-command-urls.ts index f226b6f4..8c77a542 100644 --- a/packages/extension/e2e/generated-command-urls.ts +++ b/packages/extension/e2e/generated-command-urls.ts @@ -12,22 +12,36 @@ export const COMMAND_URLS: UrlEntry[] = [ { title: "Netflix", locale: "en", searchUrl: "https://www.netflix.com/search?q=%s" }, { title: "Pinterest", locale: "en", searchUrl: "https://www.pinterest.com/search/pins/?q=%s" }, { title: "Drive", locale: "en", searchUrl: "https://drive.google.com/drive/search?q=%s" }, + { title: "Search Commands on Hub", locale: "en", searchUrl: "https://selection-command.com/en?q=%pageUrl" }, { title: "Amazon", locale: "ja", searchUrl: "https://www.amazon.co.jp/s?k=%s" }, { title: "Yahoo! Japan", locale: "ja", searchUrl: "https://search.yahoo.co.jp/search?p=%s" }, { title: "en to ja", locale: "ja", searchUrl: "https://translate.google.co.jp/?hl=ja&sl=auto&text=%s&op=translate" }, + { title: "コマンドをHubで検索", locale: "ja", searchUrl: "https://selection-command.com/ja?q=%pageUrl" }, { title: "百度", locale: "zh", searchUrl: "https://www.baidu.com/s?wd=%s" }, { title: "京东", locale: "zh", searchUrl: "https://search.jd.com/Search?keyword=%s" }, { title: "哔哩哔哩", locale: "zh", searchUrl: "https://search.bilibili.com/all?keyword=%s" }, + { title: "在 Hub 中搜索命令", locale: "zh", searchUrl: "https://selection-command.com/zh-CN?q=%pageUrl" }, { title: "네이버", locale: "ko", searchUrl: "https://search.naver.com/search.naver?query=%s" }, + { title: "Hub에서 명령어 검색", locale: "ko", searchUrl: "https://selection-command.com/ko?q=%pageUrl" }, { title: "Яндекс", locale: "ru", searchUrl: "https://yandex.ru/search/?text=%s" }, { title: "ВКонтакте", locale: "ru", searchUrl: "https://vk.com/search?c%5Bq%5D=%s" }, + { title: "Поиск команд в Hub", locale: "ru", searchUrl: "https://selection-command.com/ru?q=%pageUrl" }, + { title: "Befehle im Hub suchen", locale: "de", searchUrl: "https://selection-command.com/de?q=%pageUrl" }, + { title: "Rechercher des commandes sur Hub", locale: "fr", searchUrl: "https://selection-command.com/fr?q=%pageUrl" }, { title: "Amazon", locale: "es", searchUrl: "https://www.amazon.es/s?k=%s" }, { title: "El Corte Inglés", locale: "es", searchUrl: "https://www.elcorteingles.es/search-nwx/?s=%s" }, { title: "AliExpress", locale: "es", searchUrl: "https://es.aliexpress.com/w/wholesale-%s.html" }, + { title: "Buscar comandos en Hub", locale: "es", searchUrl: "https://selection-command.com/es?q=%pageUrl" }, + { title: "Pesquisar comandos no Hub", locale: "pt-br", searchUrl: "https://selection-command.com/pt-BR?q=%pageUrl" }, { title: "OLX", locale: "pt", searchUrl: "https://www.olx.pt/ads/?q=%s" }, + { title: "Pesquisar comandos no Hub", locale: "pt", searchUrl: "https://selection-command.com/pt-PT?q=%pageUrl" }, + { title: "Hub पर कमांड खोजें", locale: "hi", searchUrl: "https://selection-command.com/hi?q=%pageUrl" }, { title: "Tokopedia", locale: "id", searchUrl: "https://www.tokopedia.com/search?st=product&q=%s" }, { title: "Shopee", locale: "id", searchUrl: "https://shopee.co.id/search?keyword=%s" }, + { title: "Cari Perintah di Hub", locale: "id", searchUrl: "https://selection-command.com/id?q=%pageUrl" }, { title: "Shopee", locale: "ms", searchUrl: "https://shopee.com.my/search?keyword=%s" }, { title: "Lazada", locale: "ms", searchUrl: "https://www.lazada.com.my/catalog/?q=%s" }, + { title: "Cari Perintah di Hub", locale: "ms", searchUrl: "https://selection-command.com/ms?q=%pageUrl" }, { title: "Zalando.it", locale: "it", searchUrl: "https://www.zalando.it/catalogo/?q=%s" }, + { title: "Cerca comandi su Hub", locale: "it", searchUrl: "https://selection-command.com/it?q=%pageUrl" }, ] diff --git a/packages/extension/src/services/option/defaultSettings.ts b/packages/extension/src/services/option/defaultSettings.ts index 96e0f0df..87930daf 100644 --- a/packages/extension/src/services/option/defaultSettings.ts +++ b/packages/extension/src/services/option/defaultSettings.ts @@ -394,7 +394,10 @@ export const DefaultCommands = [ // variant shares COMMAND_SEARCH_ID so the settings migration can detect // whether a user already has one, regardless of which locale it was added in. function createCommandSearchCommand(title: string, hubLocale: string): Command { - const iconUrl = chrome.runtime.getURL("images/search_command.png") + const iconUrl = + typeof chrome !== "undefined" && chrome.runtime?.getURL + ? chrome.runtime.getURL("images/search_command.png") + : "" return { id: COMMAND_SEARCH_ID, title, From fbf6e0edfedd15ab4b55127d5c6e256e16d07442 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Sat, 11 Jul 2026 23:19:21 +0900 Subject: [PATCH 10/10] Fix: forward pageUrl in background-direct execution path and align SidePanel action - action/background.ts: execute() destructured params without pageUrl and never forwarded it to executeAction, so commands run via the background-direct fallback (e.g. chrome:// tabs) always got an empty pageUrl, defeating %pageUrl/srcUrl interpolation. - action/sidePanel.ts: accept pageUrl like every other action instead of reading location.href directly, for consistency and to avoid the same class of bug if SidePanel is ever invoked from a background context. - ShortcutList.tsx: deduplicate willUseClipboard/referencesSelection into a shared referencesPlaceholder helper parameterized by the placeholder predicate. Co-Authored-By: Claude Sonnet 5 --- packages/extension/src/action/background.ts | 2 + packages/extension/src/action/sidePanel.ts | 3 +- .../components/option/editor/ShortcutList.tsx | 70 ++++++++----------- 3 files changed, 35 insertions(+), 40 deletions(-) diff --git a/packages/extension/src/action/background.ts b/packages/extension/src/action/background.ts index b9a22711..23047ab2 100644 --- a/packages/extension/src/action/background.ts +++ b/packages/extension/src/action/background.ts @@ -27,6 +27,7 @@ export async function execute({ useSecondary = false, useClipboard = false, changeState, + pageUrl, }: ExecuteCommandParams) { return executeAction({ command, @@ -36,6 +37,7 @@ export async function execute({ useSecondary, useClipboard, changeState, + pageUrl, actions: actionsForBackground, }) } diff --git a/packages/extension/src/action/sidePanel.ts b/packages/extension/src/action/sidePanel.ts index 3f3cd7e5..5da4bef6 100644 --- a/packages/extension/src/action/sidePanel.ts +++ b/packages/extension/src/action/sidePanel.ts @@ -10,6 +10,7 @@ export const SidePanel = { selectionText, command, useClipboard, + pageUrl, }: ExecuteCommandParams) { if (!isValidString(command.searchUrl)) { console.error("searchUrl is not valid.") @@ -39,7 +40,7 @@ export const SidePanel = { spaceEncoding: command.spaceEncoding ?? SPACE_ENCODING.PLUS, selectionText, useClipboard: useClipboard ?? false, - pageUrl: location.href, + pageUrl: pageUrl ?? "", }, clipboardText, ) diff --git a/packages/extension/src/components/option/editor/ShortcutList.tsx b/packages/extension/src/components/option/editor/ShortcutList.tsx index 8d069153..f803ba9a 100644 --- a/packages/extension/src/components/option/editor/ShortcutList.tsx +++ b/packages/extension/src/components/option/editor/ShortcutList.tsx @@ -35,28 +35,39 @@ const hasSelectionOrClipboardPlaceholder = (text: string) => text.includes(toInsertTemplate(INSERT.SELECTED_TEXT)) || text.includes(toInsertTemplate(INSERT.CLIPBOARD)) -// Checks whether the command references the selected text or clipboard at -// all, regardless of openMode. Used to decide whether the "no selection -// behavior" setting has any effect on the command. -const willUseClipboard = (command: Command): boolean => { +const hasSelectionPlaceholder = (text: string) => + text.includes(toInsertTemplate(INSERT.SELECTED_TEXT)) + +// Checks whether the command references a placeholder matched by +// `hasPlaceholder`, regardless of openMode. +const referencesPlaceholder = ( + command: Command, + hasPlaceholder: (text: string) => boolean, +): boolean => { if (isSearchType(command) || command.openMode === OPEN_MODE.API) { return command.searchUrl?.includes("%s") ?? false } if (isPageActionType(command)) { return command.pageActionOption.steps.some((step) => - hasSelectionOrClipboardPlaceholder(paramToStr(step.param)), + hasPlaceholder(paramToStr(step.param)), ) } if (isAiPromptType(command)) { - return hasSelectionOrClipboardPlaceholder(command.aiPromptOption.prompt) + return hasPlaceholder(command.aiPromptOption.prompt) } // copy / getTextStyles / linkPopup always operate on the current selection. return true } +// Checks whether the command references the selected text or clipboard at +// all, regardless of openMode. Used to decide whether the "no selection +// behavior" setting has any effect on the command. +const willUseClipboard = (command: Command): boolean => + referencesPlaceholder(command, hasSelectionOrClipboardPlaceholder) + const isTextSelectionOnly = (command: Command) => { const { openMode } = command @@ -83,43 +94,24 @@ const isTextSelectionOnly = (command: Command) => { return !Object.values(OPEN_MODE_BG).includes(openMode as any) } -const hasSelectionPlaceholder = (text: string) => - text.includes(toInsertTemplate(INSERT.SELECTED_TEXT)) - -const referencesSelection = (command: Command): boolean => { - if (isSearchType(command) || command.openMode === OPEN_MODE.API) { - return command.searchUrl?.includes("%s") ?? false - } - - if (isPageActionType(command)) { - return command.pageActionOption.steps.some((step) => - hasSelectionPlaceholder(paramToStr(step.param)), - ) - } - - if (isAiPromptType(command)) { - return hasSelectionPlaceholder(command.aiPromptOption.prompt) - } - - // copy / getTextStyles / linkPopup always operate on the current selection. - return true -} +const referencesSelection = (command: Command): boolean => + referencesPlaceholder(command, hasSelectionPlaceholder) const createNameRender = (command: Command) => { return isTextSelectionOnly(command) ? (name: string) => ( - - {name} - - {t("shortcut_text_selection_only")} + + {name} + + {t("shortcut_text_selection_only")} + - - ) + ) : undefined } @@ -201,7 +193,7 @@ export function ShortcutList({ control }: ShortcutListProps) { cmd && isTextSelectionOnly(cmd) && shortcut?.noSelectionBehavior !== - SHORTCUT_NO_SELECTION_BEHAVIOR.DO_NOTHING + SHORTCUT_NO_SELECTION_BEHAVIOR.DO_NOTHING ) { setValue( `shortcuts.shortcuts.${index}.noSelectionBehavior`,