diff --git a/embd_res/kcpp_sdui.embd b/embd_res/kcpp_sdui.embd index 09f7a0400..7d9b27f85 100644 --- a/embd_res/kcpp_sdui.embd +++ b/embd_res/kcpp_sdui.embd @@ -40,7 +40,7 @@ m 0 ${w?"":"-"}${_} a ${_} ${_} 0 1 1 0 ${w?"-":""}${_*2} a ${_} ${_} 0 1 1 0 ${w?"":"-"}${_*2} - `}),a=ee(()=>2*Math.PI*g.value),u=ee(()=>t.type==="dashboard"?.75:1),d=ee(()=>`${-1*a.value*(1-u.value)/2}px`),c=ee(()=>({strokeDasharray:`${a.value*u.value}px, ${a.value}px`,strokeDashoffset:d.value})),f=ee(()=>({strokeDasharray:`${a.value*u.value*(t.percentage/100)}px, ${a.value}px`,strokeDashoffset:d.value,transition:"stroke-dasharray 0.6s ease 0s, stroke 0.6s ease, opacity ease 0.6s"})),s=ee(()=>{let _;return t.color?_=b(t.percentage):_=n[t.status]||n.default,_}),h=ee(()=>t.status==="warning"?bp:t.type==="line"?t.status==="success"?Ll:qc:t.status==="success"?__:$r),v=ee(()=>t.type==="line"?12+t.strokeWidth*.4:t.width*.111111+2),m=ee(()=>t.format(t.percentage));function y(_){const w=100/_.length;return _.map((x,k)=>Xe(x)?{color:x,percentage:(k+1)*w}:x).sort((x,k)=>x.percentage-k.percentage)}const b=_=>{var w;const{color:S}=t;if(Ue(S))return S(_);if(Xe(S))return S;{const x=y(S);for(const k of x)if(k.percentage>_)return k.color;return(w=x[x.length-1])==null?void 0:w.color}};return(_,w)=>(z(),se("div",{class:de([C(r).b(),C(r).m(_.type),C(r).is(_.status),{[C(r).m("without-text")]:!_.showText,[C(r).m("text-inside")]:_.textInside}]),role:"progressbar","aria-valuenow":_.percentage,"aria-valuemin":"0","aria-valuemax":"100"},[_.type==="line"?(z(),se("div",{key:0,class:de(C(r).b("bar"))},[ne("div",{class:de(C(r).be("bar","outer")),style:ze({height:`${_.strokeWidth}px`})},[ne("div",{class:de([C(r).be("bar","inner"),{[C(r).bem("bar","inner","indeterminate")]:_.indeterminate}]),style:ze(C(i))},[(_.showText||_.$slots.default)&&_.textInside?(z(),se("div",{key:0,class:de(C(r).be("bar","innerText"))},[xe(_.$slots,"default",{percentage:_.percentage},()=>[ne("span",null,Ae(C(m)),1)])],2)):ye("v-if",!0)],6)],6)],2)):(z(),se("div",{key:1,class:de(C(r).b("circle")),style:ze({height:`${_.width}px`,width:`${_.width}px`})},[(z(),se("svg",N9,[ne("path",{class:de(C(r).be("circle","track")),d:C(o),stroke:`var(${C(r).cssVarName("fill-color-light")}, #e5e9f2)`,"stroke-width":C(l),fill:"none",style:ze(C(c))},null,14,j9),ne("path",{class:de(C(r).be("circle","path")),d:C(o),stroke:C(s),fill:"none",opacity:_.percentage?1:0,"stroke-linecap":_.strokeLinecap,"stroke-width":C(l),style:ze(C(f))},null,14,V9)]))],6)),(_.showText||_.$slots.default)&&!_.textInside?(z(),se("div",{key:2,class:de(C(r).e("text")),style:ze({fontSize:`${C(v)}px`})},[xe(_.$slots,"default",{percentage:_.percentage},()=>[_.status?(z(),be(C(Me),{key:1},{default:he(()=>[(z(),be(Ct(C(h))))]),_:1})):(z(),se("span",U9,Ae(C(m)),1))])],6)):ye("v-if",!0)],10,z9))}});var Y9=Le(W9,[["__file","/home/runner/work/element-plus/element-plus/packages/components/progress/src/progress.vue"]]);const Ew=kt(Y9),X9=["start","center","end","space-around","space-between","space-evenly"],K9=["top","middle","bottom"],G9=Fe({tag:{type:String,default:"div"},gutter:{type:Number,default:0},justify:{type:String,values:X9,default:"start"},align:{type:String,values:K9,default:"top"}}),q9={name:"ElRow"},Z9=Ee({...q9,props:G9,setup(e){const t=e,n=Ie("row"),r=ee(()=>t.gutter);gt(T1,{gutter:r});const i=ee(()=>{const l={};return t.gutter&&(l.marginRight=l.marginLeft=`-${t.gutter/2}px`),l});return(l,g)=>(z(),be(Ct(l.tag),{class:de([C(n).b(),C(n).is(`justify-${t.justify}`,l.justify!=="start"),C(n).is(`align-${t.align}`,l.align!=="top")]),style:ze(C(i))},{default:he(()=>[xe(l.$slots,"default")]),_:3},8,["class","style"]))}});var J9=Le(Z9,[["__file","/home/runner/work/element-plus/element-plus/packages/components/row/src/row.vue"]]);const T0=kt(J9),Q9=Fe({modelValue:{type:Pe([Number,Array]),default:0},id:{type:String,default:void 0},min:{type:Number,default:0},max:{type:Number,default:100},step:{type:Number,default:1},showInput:Boolean,showInputControls:{type:Boolean,default:!0},size:Ki,inputSize:Ki,showStops:Boolean,showTooltip:{type:Boolean,default:!0},formatTooltip:{type:Pe(Function),default:void 0},disabled:Boolean,range:Boolean,vertical:Boolean,height:String,debounce:{type:Number,default:300},label:{type:String,default:void 0},rangeStartLabel:{type:String,default:void 0},rangeEndLabel:{type:String,default:void 0},formatValueText:{type:Pe(Function),default:void 0},tooltipClass:{type:String,default:void 0},placement:{type:String,values:Go,default:"top"},marks:{type:Pe(Object)},validateEvent:{type:Boolean,default:!0}}),Uf=e=>ft(e)||Ve(e)&&e.every(ft),ej={[wt]:Uf,[Ba]:Uf,[ui]:Uf},tj=(e,t,n)=>{const r=oe();return rt(async()=>{e.range?(Array.isArray(e.modelValue)?(t.firstValue=Math.max(e.min,e.modelValue[0]),t.secondValue=Math.min(e.max,e.modelValue[1])):(t.firstValue=e.min,t.secondValue=e.max),t.oldValue=[t.firstValue,t.secondValue]):(typeof e.modelValue!="number"||Number.isNaN(e.modelValue)?t.firstValue=e.min:t.firstValue=Math.min(e.max,Math.max(e.min,e.modelValue)),t.oldValue=t.firstValue),St(window,"resize",n),await He(),n()}),{sliderWrapper:r}},nj=e=>ee(()=>e.marks?Object.keys(e.marks).map(Number.parseFloat).sort((n,r)=>n-r).filter(n=>n<=e.max&&n>=e.min).map(n=>({point:n,position:(n-e.min)*100/(e.max-e.min),mark:e.marks[n]})):[]),rj=(e,t,n)=>{const{form:r,formItem:i}=ea(),l=gn(),g=oe(),o=oe(),a={firstButton:g,secondButton:o},u=ee(()=>e.disabled||(r==null?void 0:r.disabled)||!1),d=ee(()=>Math.min(t.firstValue,t.secondValue)),c=ee(()=>Math.max(t.firstValue,t.secondValue)),f=ee(()=>e.range?`${100*(c.value-d.value)/(e.max-e.min)}%`:`${100*(t.firstValue-e.min)/(e.max-e.min)}%`),s=ee(()=>e.range?`${100*(d.value-e.min)/(e.max-e.min)}%`:"0%"),h=ee(()=>e.vertical?{height:e.height}:{}),v=ee(()=>e.vertical?{height:f.value,bottom:s.value}:{width:f.value,left:s.value}),m=()=>{l.value&&(t.sliderSize=l.value[`client${e.vertical?"Height":"Width"}`])},y=N=>{const L=e.min+N*(e.max-e.min)/100;if(!e.range)return g;let F;return Math.abs(d.value-L)t.secondValue?"firstButton":"secondButton",a[F]},b=N=>{const L=y(N);return L.value.setPosition(N),L},_=N=>{t.firstValue=N,S(e.range?[d.value,c.value]:N)},w=N=>{t.secondValue=N,e.range&&S([d.value,c.value])},S=N=>{n(wt,N),n(Ba,N)},x=async()=>{await He(),n(ui,e.range?[d.value,c.value]:e.modelValue)},k=N=>{var L,F,O,V,re,J;if(u.value||t.dragging)return;m();let ue=0;if(e.vertical){const M=(O=(F=(L=N.touches)==null?void 0:L.item(0))==null?void 0:F.clientY)!=null?O:N.clientY;ue=(l.value.getBoundingClientRect().bottom-M)/t.sliderSize*100}else{const M=(J=(re=(V=N.touches)==null?void 0:V.item(0))==null?void 0:re.clientX)!=null?J:N.clientX,H=l.value.getBoundingClientRect().left;ue=(M-H)/t.sliderSize*100}if(!(ue<0||ue>100))return b(ue)};return{elFormItem:i,slider:l,firstButton:g,secondButton:o,sliderDisabled:u,minValue:d,maxValue:c,runwayStyle:h,barStyle:v,resetSize:m,setPosition:b,emitChange:x,onSliderWrapperPrevent:N=>{var L,F;(((L=a.firstButton.value)==null?void 0:L.dragging)||((F=a.secondButton.value)==null?void 0:F.dragging))&&N.preventDefault()},onSliderClick:N=>{k(N)&&x()},onSliderDown:async N=>{const L=k(N);L&&(await He(),L.value.onButtonDown(N))},setFirstValue:_,setSecondValue:w}},{left:ij,down:aj,right:oj,up:sj,home:lj,end:uj,pageUp:cj,pageDown:fj}=Ot,dj=(e,t,n)=>{const r=oe(),i=oe(!1),l=ee(()=>t.value instanceof Function),g=ee(()=>l.value&&t.value(e.modelValue)||e.modelValue),o=Ro(()=>{n.value&&(i.value=!0)},50),a=Ro(()=>{n.value&&(i.value=!1)},50);return{tooltip:r,tooltipVisible:i,formatValue:g,displayTooltip:o,hideTooltip:a}},hj=(e,t,n)=>{const{disabled:r,min:i,max:l,step:g,showTooltip:o,precision:a,sliderSize:u,formatTooltip:d,emitChange:c,resetSize:f,updateDragging:s}=Ke(O1),{tooltip:h,tooltipVisible:v,formatValue:m,displayTooltip:y,hideTooltip:b}=dj(e,d,o),_=oe(),w=ee(()=>`${(e.modelValue-i.value)/(l.value-i.value)*100}%`),S=ee(()=>e.vertical?{bottom:w.value}:{left:w.value}),x=()=>{t.hovering=!0,y()},k=()=>{t.hovering=!1,t.dragging||b()},E=P=>{r.value||(P.preventDefault(),ue(P),window.addEventListener("mousemove",M),window.addEventListener("touchmove",M),window.addEventListener("mouseup",H),window.addEventListener("touchend",H),window.addEventListener("contextmenu",H),_.value.focus())},I=P=>{r.value||(t.newPosition=Number.parseFloat(w.value)+P/(l.value-i.value)*100,R(t.newPosition),c())},D=()=>{I(-g.value)},N=()=>{I(g.value)},L=()=>{I(-g.value*4)},F=()=>{I(g.value*4)},O=()=>{r.value||(R(0),c())},V=()=>{r.value||(R(100),c())},re=P=>{let Q=!0;[ij,aj].includes(P.key)?D():[oj,sj].includes(P.key)?N():P.key===lj?O():P.key===uj?V():P.key===fj?L():P.key===cj?F():Q=!1,Q&&P.preventDefault()},J=P=>{let Q,j;return P.type.startsWith("touch")?(j=P.touches[0].clientY,Q=P.touches[0].clientX):(j=P.clientY,Q=P.clientX),{clientX:Q,clientY:j}},ue=P=>{t.dragging=!0,t.isClick=!0;const{clientX:Q,clientY:j}=J(P);e.vertical?t.startY=j:t.startX=Q,t.startPosition=Number.parseFloat(w.value),t.newPosition=t.startPosition},M=P=>{if(t.dragging){t.isClick=!1,y(),f();let Q;const{clientX:j,clientY:A}=J(P);e.vertical?(t.currentY=A,Q=(t.startY-t.currentY)/u.value*100):(t.currentX=j,Q=(t.currentX-t.startX)/u.value*100),t.newPosition=t.startPosition+Q,R(t.newPosition)}},H=()=>{t.dragging&&(setTimeout(()=>{t.dragging=!1,t.hovering||b(),t.isClick||R(t.newPosition),c()},0),window.removeEventListener("mousemove",M),window.removeEventListener("touchmove",M),window.removeEventListener("mouseup",H),window.removeEventListener("touchend",H),window.removeEventListener("contextmenu",H))},R=async P=>{if(P===null||Number.isNaN(+P))return;P<0?P=0:P>100&&(P=100);const Q=100/((l.value-i.value)/g.value);let A=Math.round(P/Q)*Q*(l.value-i.value)*.01+i.value;A=Number.parseFloat(A.toFixed(a.value)),A!==e.modelValue&&n(wt,A),!t.dragging&&e.modelValue!==t.oldValue&&(t.oldValue=e.modelValue),await He(),t.dragging&&y(),h.value.updatePopper()};return ke(()=>t.dragging,P=>{s(P)}),{disabled:r,button:_,tooltip:h,tooltipVisible:v,showTooltip:o,wrapperStyle:S,formatValue:m,handleMouseEnter:x,handleMouseLeave:k,onButtonDown:E,onKeyDown:re,setPosition:R}},pj=(e,t,n,r)=>({stops:ee(()=>{if(!e.showStops||e.min>e.max)return[];if(e.step===0)return[];const g=(e.max-e.min)/e.step,o=100*e.step/(e.max-e.min),a=Array.from({length:g-1}).map((u,d)=>(d+1)*o);return e.range?a.filter(u=>u<100*(n.value-e.min)/(e.max-e.min)||u>100*(r.value-e.min)/(e.max-e.min)):a.filter(u=>u>100*(t.firstValue-e.min)/(e.max-e.min))}),getStopStyle:g=>e.vertical?{bottom:`${g}%`}:{left:`${g}%`}}),mj=(e,t,n,r,i,l)=>{const g=u=>{i(wt,u),i(Ba,u)},o=()=>e.range?![n.value,r.value].every((u,d)=>u===t.oldValue[d]):e.modelValue!==t.oldValue,a=()=>{var u,d;if(e.min>e.max){Tn("Slider","min should not be greater than max.");return}const c=e.modelValue;e.range&&Array.isArray(c)?c[1]e.max?g([e.max,e.max]):c[0]e.max?g([c[0],e.max]):(t.firstValue=c[0],t.secondValue=c[1],o()&&(e.validateEvent&&((u=l==null?void 0:l.validate)==null||u.call(l,"change").catch(f=>void 0)),t.oldValue=c.slice())):!e.range&&typeof c=="number"&&!Number.isNaN(c)&&(ce.max?g(e.max):(t.firstValue=c,o()&&(e.validateEvent&&((d=l==null?void 0:l.validate)==null||d.call(l,"change").catch(f=>void 0)),t.oldValue=c)))};a(),ke(()=>t.dragging,u=>{u||a()}),ke(()=>e.modelValue,(u,d)=>{t.dragging||Array.isArray(u)&&Array.isArray(d)&&u.every((c,f)=>c===d[f])&&t.firstValue===u[0]&&t.secondValue===u[1]||a()},{deep:!0}),ke(()=>[e.min,e.max],()=>{a()})},gj=Fe({modelValue:{type:Number,default:0},vertical:Boolean,tooltipClass:String,placement:{type:String,values:Go,default:"top"}}),vj={[wt]:e=>ft(e)},yj=["tabindex"],bj={name:"ElSliderButton"},_j=Ee({...bj,props:gj,emits:vj,setup(e,{expose:t,emit:n}){const r=e,i=Ie("slider"),l=vt({hovering:!1,dragging:!1,isClick:!1,startX:0,currentX:0,startY:0,currentY:0,startPosition:0,newPosition:0,oldValue:r.modelValue}),{disabled:g,button:o,tooltip:a,showTooltip:u,tooltipVisible:d,wrapperStyle:c,formatValue:f,handleMouseEnter:s,handleMouseLeave:h,onButtonDown:v,onKeyDown:m,setPosition:y}=hj(r,l,n),{hovering:b,dragging:_}=rr(l);return t({onButtonDown:v,onKeyDown:m,setPosition:y,hovering:b,dragging:_}),(w,S)=>(z(),se("div",{ref_key:"button",ref:o,class:de([C(i).e("button-wrapper"),{hover:C(b),dragging:C(_)}]),style:ze(C(c)),tabindex:C(g)?-1:0,onMouseenter:S[0]||(S[0]=(...x)=>C(s)&&C(s)(...x)),onMouseleave:S[1]||(S[1]=(...x)=>C(h)&&C(h)(...x)),onMousedown:S[2]||(S[2]=(...x)=>C(v)&&C(v)(...x)),onTouchstart:S[3]||(S[3]=(...x)=>C(v)&&C(v)(...x)),onFocus:S[4]||(S[4]=(...x)=>C(s)&&C(s)(...x)),onBlur:S[5]||(S[5]=(...x)=>C(h)&&C(h)(...x)),onKeydown:S[6]||(S[6]=(...x)=>C(m)&&C(m)(...x))},[le(C(Vn),{ref_key:"tooltip",ref:a,visible:C(d),placement:w.placement,"fallback-placements":["top","bottom","right","left"],"stop-popper-mouse-event":!1,"popper-class":w.tooltipClass,disabled:!C(u),persistent:""},{content:he(()=>[ne("span",null,Ae(C(f)),1)]),default:he(()=>[ne("div",{class:de([C(i).e("button"),{hover:C(b),dragging:C(_)}])},null,2)]),_:1},8,["visible","placement","popper-class","disabled"])],46,yj))}});var E0=Le(_j,[["__file","/home/runner/work/element-plus/element-plus/packages/components/slider/src/button.vue"]]);const wj=Fe({mark:{type:Pe([String,Object]),default:void 0}});var Cj=Ee({name:"ElSliderMarker",props:wj,setup(e){const t=Ie("slider"),n=ee(()=>Xe(e.mark)?e.mark:e.mark.label),r=ee(()=>Xe(e.mark)?void 0:e.mark.style);return()=>_t("div",{class:t.e("marks-text"),style:r.value},n.value)}});const Sj=["id","role","aria-label","aria-labelledby"],xj={key:1},kj={name:"ElSlider"},Tj=Ee({...kj,props:Q9,emits:ej,setup(e,{expose:t,emit:n}){const r=e,i=Ie("slider"),{t:l}=yn(),g=vt({firstValue:0,secondValue:0,oldValue:0,dragging:!1,sliderSize:1}),{elFormItem:o,slider:a,firstButton:u,secondButton:d,sliderDisabled:c,minValue:f,maxValue:s,runwayStyle:h,barStyle:v,resetSize:m,emitChange:y,onSliderWrapperPrevent:b,onSliderClick:_,onSliderDown:w,setFirstValue:S,setSecondValue:x}=rj(r,g,n),{stops:k,getStopStyle:E}=pj(r,g,f,s),{inputId:I,isLabeledByFormItem:D}=$l(r,{formItemContext:o}),N=sr(),L=ee(()=>r.inputSize||N.value),F=ee(()=>r.label||l("el.slider.defaultLabel",{min:r.min,max:r.max})),O=ee(()=>r.range?r.rangeStartLabel||l("el.slider.defaultRangeStartLabel"):F.value),V=ee(()=>r.formatValueText?r.formatValueText(P.value):`${P.value}`),re=ee(()=>r.rangeEndLabel||l("el.slider.defaultRangeEndLabel")),J=ee(()=>r.formatValueText?r.formatValueText(Q.value):`${Q.value}`),ue=ee(()=>[i.b(),i.m(N.value),i.is("vertical",r.vertical),{[i.m("with-input")]:r.showInput}]),M=nj(r);mj(r,g,f,s,n,o);const H=ee(()=>{const q=[r.min,r.max,r.step].map(G=>{const te=`${G}`.split(".")[1];return te?te.length:0});return Math.max.apply(null,q)}),{sliderWrapper:R}=tj(r,g,m),{firstValue:P,secondValue:Q,sliderSize:j}=rr(g),A=q=>{g.dragging=q};return gt(O1,{...rr(r),sliderSize:j,disabled:c,precision:H,emitChange:y,resetSize:m,updateDragging:A}),t({onSliderClick:_}),(q,G)=>{var te,fe;return z(),se("div",{id:q.range?C(I):void 0,ref_key:"sliderWrapper",ref:R,class:de(C(ue)),role:q.range?"group":void 0,"aria-label":q.range&&!C(D)?C(F):void 0,"aria-labelledby":q.range&&C(D)?(te=C(o))==null?void 0:te.labelId:void 0,onTouchstart:G[2]||(G[2]=(...pe)=>C(b)&&C(b)(...pe)),onTouchmove:G[3]||(G[3]=(...pe)=>C(b)&&C(b)(...pe))},[ne("div",{ref_key:"slider",ref:a,class:de([C(i).e("runway"),{"show-input":q.showInput&&!q.range},C(i).is("disabled",C(c))]),style:ze(C(h)),onMousedown:G[0]||(G[0]=(...pe)=>C(w)&&C(w)(...pe)),onTouchstart:G[1]||(G[1]=(...pe)=>C(w)&&C(w)(...pe))},[ne("div",{class:de(C(i).e("bar")),style:ze(C(v))},null,6),le(E0,{id:q.range?void 0:C(I),ref_key:"firstButton",ref:u,"model-value":C(P),vertical:q.vertical,"tooltip-class":q.tooltipClass,placement:q.placement,role:"slider","aria-label":q.range||!C(D)?C(O):void 0,"aria-labelledby":!q.range&&C(D)?(fe=C(o))==null?void 0:fe.labelId:void 0,"aria-valuemin":q.min,"aria-valuemax":q.range?C(Q):q.max,"aria-valuenow":C(P),"aria-valuetext":C(V),"aria-orientation":q.vertical?"vertical":"horizontal","aria-disabled":C(c),"onUpdate:modelValue":C(S)},null,8,["id","model-value","vertical","tooltip-class","placement","aria-label","aria-labelledby","aria-valuemin","aria-valuemax","aria-valuenow","aria-valuetext","aria-orientation","aria-disabled","onUpdate:modelValue"]),q.range?(z(),be(E0,{key:0,ref_key:"secondButton",ref:d,"model-value":C(Q),vertical:q.vertical,"tooltip-class":q.tooltipClass,placement:q.placement,role:"slider","aria-label":C(re),"aria-valuemin":C(P),"aria-valuemax":q.max,"aria-valuenow":C(Q),"aria-valuetext":C(J),"aria-orientation":q.vertical?"vertical":"horizontal","aria-disabled":C(c),"onUpdate:modelValue":C(x)},null,8,["model-value","vertical","tooltip-class","placement","aria-label","aria-valuemin","aria-valuemax","aria-valuenow","aria-valuetext","aria-orientation","aria-disabled","onUpdate:modelValue"])):ye("v-if",!0),q.showStops?(z(),se("div",xj,[(z(!0),se(Ne,null,Nt(C(k),(pe,ve)=>(z(),se("div",{key:ve,class:de(C(i).e("stop")),style:ze(C(E)(pe))},null,6))),128))])):ye("v-if",!0),C(M).length>0?(z(),se(Ne,{key:2},[ne("div",null,[(z(!0),se(Ne,null,Nt(C(M),(pe,ve)=>(z(),se("div",{key:ve,style:ze(C(E)(pe.position)),class:de([C(i).e("stop"),C(i).e("marks-stop")])},null,6))),128))]),ne("div",{class:de(C(i).e("marks"))},[(z(!0),se(Ne,null,Nt(C(M),(pe,ve)=>(z(),be(C(Cj),{key:ve,mark:pe.mark,style:ze(C(E)(pe.position))},null,8,["mark","style"]))),128))],2)],64)):ye("v-if",!0)],38),q.showInput&&!q.range?(z(),be(C(ih),{key:0,ref:"input","model-value":C(P),class:de(C(i).e("input")),step:q.step,disabled:C(c),controls:q.showInputControls,min:q.min,max:q.max,debounce:q.debounce,size:C(L),"onUpdate:modelValue":C(S),onChange:C(y)},null,8,["model-value","class","step","disabled","controls","min","max","debounce","size","onUpdate:modelValue","onChange"])):ye("v-if",!0)],42,Sj)}}});var Ej=Le(Tj,[["__file","/home/runner/work/element-plus/element-plus/packages/components/slider/src/slider.vue"]]);const O0=kt(Ej),Oj=Fe({modelValue:{type:[Boolean,String,Number],default:!1},value:{type:[Boolean,String,Number],default:!1},disabled:{type:Boolean,default:!1},width:{type:[String,Number],default:""},inlinePrompt:{type:Boolean,default:!1},activeIcon:{type:Wt},inactiveIcon:{type:Wt},activeText:{type:String,default:""},inactiveText:{type:String,default:""},activeColor:{type:String,default:""},inactiveColor:{type:String,default:""},borderColor:{type:String,default:""},activeValue:{type:[Boolean,String,Number],default:!0},inactiveValue:{type:[Boolean,String,Number],default:!1},name:{type:String,default:""},validateEvent:{type:Boolean,default:!0},id:String,loading:{type:Boolean,default:!1},beforeChange:{type:Pe(Function)},size:{type:String,validator:nf},tabindex:{type:[String,Number]}}),Aj={[wt]:e=>Yn(e)||Xe(e)||ft(e),[ui]:e=>Yn(e)||Xe(e)||ft(e),[Ba]:e=>Yn(e)||Xe(e)||ft(e)},Pj=["onClick"],Ij=["id","aria-checked","aria-disabled","name","true-value","false-value","disabled","tabindex","onKeydown"],Mj=["aria-hidden"],Lj=["aria-hidden"],Rj=["aria-hidden"],Dj=["aria-hidden"],$j={name:"ElSwitch"},Bj=Ee({...$j,props:Oj,emits:Aj,setup(e,{expose:t,emit:n}){const r=e,i="ElSwitch",l=xt(),{formItem:g}=ea(),o=sr(),a=Ie("switch");al({from:'"value"',replacement:'"model-value" or "v-model"',scope:i,version:"2.3.0",ref:"https://element-plus.org/en-US/component/switch.html#attributes",type:"Attribute"},ee(()=>{var x;return!!((x=l.vnode.props)!=null&&x.value)}));const{inputId:u}=$l(r,{formItemContext:g}),d=Ja(ee(()=>r.loading)),c=oe(r.modelValue!==!1),f=oe(),s=oe(),h=ee(()=>[a.b(),a.m(o.value),a.is("disabled",d.value),a.is("checked",y.value)]),v=ee(()=>({width:Fr(r.width)}));ke(()=>r.modelValue,()=>{c.value=!0}),ke(()=>r.value,()=>{c.value=!1});const m=ee(()=>c.value?r.modelValue:r.value),y=ee(()=>m.value===r.activeValue);[r.activeValue,r.inactiveValue].includes(m.value)||(n(wt,r.inactiveValue),n(ui,r.inactiveValue),n(Ba,r.inactiveValue)),ke(y,x=>{var k;f.value.checked=x,r.validateEvent&&((k=g==null?void 0:g.validate)==null||k.call(g,"change").catch(E=>void 0))});const b=()=>{const x=y.value?r.inactiveValue:r.activeValue;n(wt,x),n(ui,x),n(Ba,x),He(()=>{f.value.checked=y.value})},_=()=>{if(d.value)return;const{beforeChange:x}=r;if(!x){b();return}const k=x();[nc(k),Yn(k)].includes(!0)||Tn(i,"beforeChange must return type `Promise` or `boolean`"),nc(k)?k.then(I=>{I&&b()}).catch(I=>{}):k&&b()},w=ee(()=>a.cssVarBlock({...r.activeColor?{"on-color":r.activeColor}:null,...r.inactiveColor?{"off-color":r.inactiveColor}:null,...r.borderColor?{"border-color":r.borderColor}:null})),S=()=>{var x,k;(k=(x=f.value)==null?void 0:x.focus)==null||k.call(x)};return rt(()=>{f.value.checked=y.value}),t({focus:S,checked:y}),(x,k)=>(z(),se("div",{class:de(C(h)),style:ze(C(w)),onClick:tt(_,["prevent"])},[ne("input",{id:C(u),ref_key:"input",ref:f,class:de(C(a).e("input")),type:"checkbox",role:"switch","aria-checked":C(y),"aria-disabled":C(d),name:x.name,"true-value":x.activeValue,"false-value":x.inactiveValue,disabled:C(d),tabindex:x.tabindex,onChange:b,onKeydown:$t(_,["enter"])},null,42,Ij),!x.inlinePrompt&&(x.inactiveIcon||x.inactiveText)?(z(),se("span",{key:0,class:de([C(a).e("label"),C(a).em("label","left"),C(a).is("active",!C(y))])},[x.inactiveIcon?(z(),be(C(Me),{key:0},{default:he(()=>[(z(),be(Ct(x.inactiveIcon)))]),_:1})):ye("v-if",!0),!x.inactiveIcon&&x.inactiveText?(z(),se("span",{key:1,"aria-hidden":C(y)},Ae(x.inactiveText),9,Mj)):ye("v-if",!0)],2)):ye("v-if",!0),ne("span",{ref_key:"core",ref:s,class:de(C(a).e("core")),style:ze(C(v))},[x.inlinePrompt?(z(),se("div",{key:0,class:de(C(a).e("inner"))},[x.activeIcon||x.inactiveIcon?(z(),se(Ne,{key:0},[x.activeIcon?(z(),be(C(Me),{key:0,class:de([C(a).is("icon"),C(y)?C(a).is("show"):C(a).is("hide")])},{default:he(()=>[(z(),be(Ct(x.activeIcon)))]),_:1},8,["class"])):ye("v-if",!0),x.inactiveIcon?(z(),be(C(Me),{key:1,class:de([C(a).is("icon"),C(y)?C(a).is("hide"):C(a).is("show")])},{default:he(()=>[(z(),be(Ct(x.inactiveIcon)))]),_:1},8,["class"])):ye("v-if",!0)],64)):x.activeText||x.inactiveIcon?(z(),se(Ne,{key:1},[x.activeText?(z(),se("span",{key:0,class:de([C(a).is("text"),C(y)?C(a).is("show"):C(a).is("hide")]),"aria-hidden":!C(y)},Ae(x.activeText.substring(0,3)),11,Lj)):ye("v-if",!0),x.inactiveText?(z(),se("span",{key:1,class:de([C(a).is("text"),C(y)?C(a).is("hide"):C(a).is("show")]),"aria-hidden":C(y)},Ae(x.inactiveText.substring(0,3)),11,Rj)):ye("v-if",!0)],64)):ye("v-if",!0)],2)):ye("v-if",!0),ne("div",{class:de(C(a).e("action"))},[x.loading?(z(),be(C(Me),{key:0,class:de(C(a).is("loading"))},{default:he(()=>[le(C(gp))]),_:1},8,["class"])):ye("v-if",!0)],2)],6),!x.inlinePrompt&&(x.activeIcon||x.activeText)?(z(),se("span",{key:1,class:de([C(a).e("label"),C(a).em("label","right"),C(a).is("active",C(y))])},[x.activeIcon?(z(),be(C(Me),{key:0},{default:he(()=>[(z(),be(Ct(x.activeIcon)))]),_:1})):ye("v-if",!0),!x.activeIcon&&x.activeText?(z(),se("span",{key:1,"aria-hidden":!C(y)},Ae(x.activeText),9,Dj)):ye("v-if",!0)],2)):ye("v-if",!0)],14,Pj))}});var Fj=Le(Bj,[["__file","/home/runner/work/element-plus/element-plus/packages/components/switch/src/switch.vue"]]);const A0=kt(Fj),zj=Fe({tabs:{type:Pe(Array),default:()=>sn([])}}),Nj={name:"ElTabBar"},jj=Ee({...Nj,props:zj,setup(e,{expose:t}){const n=e,r="ElTabBar",i=xt(),l=Ke(rf);l||Tn(r,"");const g=Ie("tabs"),o=oe(),a=oe(),u=()=>{let c=0,f=0;const s=["top","bottom"].includes(l.props.tabPosition)?"width":"height",h=s==="width"?"x":"y";return n.tabs.every(v=>{var m,y,b,_;const w=(y=(m=i.parent)==null?void 0:m.refs)==null?void 0:y[`tab-${v.uid}`];if(!w)return!1;if(!v.active)return!0;f=w[`client${ya(s)}`];const S=h==="x"?"left":"top";c=w.getBoundingClientRect()[S]-((_=(b=w.parentElement)==null?void 0:b.getBoundingClientRect()[S])!=null?_:0);const x=window.getComputedStyle(w);return s==="width"&&(n.tabs.length>1&&(f-=Number.parseFloat(x.paddingLeft)+Number.parseFloat(x.paddingRight)),c+=Number.parseFloat(x.paddingLeft)),!1}),{[s]:`${f}px`,transform:`translate${ya(h)}(${c}px)`}},d=()=>a.value=u();return ke(()=>n.tabs,async()=>{await He(),d()},{immediate:!0}),Nr(o,()=>d()),t({ref:o,update:d}),(c,f)=>(z(),se("div",{ref_key:"barRef",ref:o,class:de([C(g).e("active-bar"),C(g).is(C(l).props.tabPosition)]),style:ze(a.value)},null,6))}});var Vj=Le(jj,[["__file","/home/runner/work/element-plus/element-plus/packages/components/tabs/src/tab-bar.vue"]]);const Uj=Fe({panes:{type:Pe(Array),default:()=>sn([])},currentName:{type:[String,Number],default:""},editable:Boolean,type:{type:String,values:["card","border-card",""],default:""},stretch:Boolean}),Hj={tabClick:(e,t,n)=>n instanceof Event,tabRemove:(e,t)=>t instanceof Event},P0="ElTabNav",Wj=Ee({name:P0,props:Uj,emits:Hj,setup(e,{expose:t,emit:n}){const r=xt(),i=Ke(rf);i||Tn(P0,"");const l=Ie("tabs"),g=dM(),o=TM(),a=oe(),u=oe(),d=oe(),c=oe(!1),f=oe(0),s=oe(!1),h=oe(!0),v=ee(()=>["top","bottom"].includes(i.props.tabPosition)?"width":"height"),m=ee(()=>({transform:`translate${v.value==="width"?"X":"Y"}(-${f.value}px)`})),y=()=>{if(!a.value)return;const E=a.value[`offset${ya(v.value)}`],I=f.value;if(!I)return;const D=I>E?I-E:0;f.value=D},b=()=>{if(!a.value||!u.value)return;const E=u.value[`offset${ya(v.value)}`],I=a.value[`offset${ya(v.value)}`],D=f.value;if(E-D<=I)return;const N=E-D>I*2?D+I:E-I;f.value=N},_=async()=>{const E=u.value;if(!c.value||!d.value||!a.value||!E)return;await He();const I=d.value.querySelector(".is-active");if(!I)return;const D=a.value,N=["top","bottom"].includes(i.props.tabPosition),L=I.getBoundingClientRect(),F=D.getBoundingClientRect(),O=N?E.offsetWidth-F.width:E.offsetHeight-F.height,V=f.value;let re=V;N?(L.leftF.right&&(re=V+L.right-F.right)):(L.topF.bottom&&(re=V+(L.bottom-F.bottom))),re=Math.max(re,0),f.value=Math.min(re,O)},w=()=>{if(!u.value||!a.value)return;const E=u.value[`offset${ya(v.value)}`],I=a.value[`offset${ya(v.value)}`],D=f.value;if(I0&&(f.value=0)},S=E=>{const I=E.code,{up:D,down:N,left:L,right:F}=Ot;if(![D,N,L,F].includes(I))return;const O=Array.from(E.currentTarget.querySelectorAll("[role=tab]:not(.is-disabled)")),V=O.indexOf(E.target);let re;I===L||I===D?V===0?re=O.length-1:re=V-1:V{h.value&&(s.value=!0)},k=()=>s.value=!1;return ke(g,E=>{E==="hidden"?h.value=!1:E==="visible"&&setTimeout(()=>h.value=!0,50)}),ke(o,E=>{E?setTimeout(()=>h.value=!0,50):h.value=!1}),Nr(d,w),rt(()=>setTimeout(()=>_(),0)),Ga(()=>w()),t({scrollToActiveTab:_,removeFocus:k}),ke(()=>e.panes,()=>r.update(),{flush:"post"}),()=>{const E=c.value?[le("span",{class:[l.e("nav-prev"),l.is("disabled",!c.value.prev)],onClick:y},[le(Me,null,{default:()=>[le(Gc,null,null)]})]),le("span",{class:[l.e("nav-next"),l.is("disabled",!c.value.next)],onClick:b},[le(Me,null,{default:()=>[le(Ml,null,null)]})])]:null,I=e.panes.map((D,N)=>{var L,F,O,V;const re=D.uid,J=D.props.disabled,ue=(F=(L=D.props.name)!=null?L:D.index)!=null?F:`${N}`,M=!J&&(D.isClosable||e.editable);D.index=`${N}`;const H=M?le(Me,{class:"is-icon-close",onClick:Q=>n("tabRemove",D,Q)},{default:()=>[le($r,null,null)]}):null,R=((V=(O=D.slots).label)==null?void 0:V.call(O))||D.props.label,P=!J&&D.active?0:-1;return le("div",{ref:`tab-${re}`,class:[l.e("item"),l.is(i.props.tabPosition),l.is("active",D.active),l.is("disabled",J),l.is("closable",M),l.is("focus",s.value)],id:`tab-${ue}`,key:`tab-${re}`,"aria-controls":`pane-${ue}`,role:"tab","aria-selected":D.active,tabindex:P,onFocus:()=>x(),onBlur:()=>k(),onClick:Q=>{k(),n("tabClick",D,ue,Q)},onKeydown:Q=>{M&&(Q.code===Ot.delete||Q.code===Ot.backspace)&&n("tabRemove",D,Q)}},[R,H])});return le("div",{ref:d,class:[l.e("nav-wrap"),l.is("scrollable",!!c.value),l.is(i.props.tabPosition)]},[E,le("div",{class:l.e("nav-scroll"),ref:a},[le("div",{class:[l.e("nav"),l.is(i.props.tabPosition),l.is("stretch",e.stretch&&["top","bottom"].includes(i.props.tabPosition))],ref:u,style:m.value,role:"tablist",onKeydown:S},[e.type?null:le(Vj,{tabs:[...e.panes]},null),I])])])}}}),Yj=Fe({type:{type:String,values:["card","border-card",""],default:""},activeName:{type:[String,Number]},closable:Boolean,addable:Boolean,modelValue:{type:[String,Number]},editable:Boolean,tabPosition:{type:String,values:["top","right","bottom","left"],default:"top"},beforeLeave:{type:Pe(Function),default:()=>!0},stretch:Boolean}),Hf=e=>Xe(e)||ft(e),Xj={[wt]:e=>Hf(e),tabClick:(e,t)=>t instanceof Event,tabChange:e=>Hf(e),edit:(e,t)=>["remove","add"].includes(t),tabRemove:e=>Hf(e),tabAdd:()=>!0};var Kj=Ee({name:"ElTabs",props:Yj,emits:Xj,setup(e,{emit:t,slots:n,expose:r}){var i,l;const g=Ie("tabs"),o=oe(),a=vt({}),u=oe((l=(i=e.modelValue)!=null?i:e.activeName)!=null?l:"0"),d=v=>{u.value=v,t(wt,v),t("tabChange",v)},c=async v=>{var m,y,b;if(!(u.value===v||pr(v)))try{await((m=e.beforeLeave)==null?void 0:m.call(e,v,u.value))!==!1&&(d(v),(b=(y=o.value)==null?void 0:y.removeFocus)==null||b.call(y))}catch{}},f=(v,m,y)=>{v.props.disabled||(c(m),t("tabClick",v,y))},s=(v,m)=>{v.props.disabled||pr(v.props.name)||(m.stopPropagation(),t("edit",v.props.name,"remove"),t("tabRemove",v.props.name))},h=()=>{t("edit",void 0,"add"),t("tabAdd")};return al({from:'"activeName"',replacement:'"model-value" or "v-model"',scope:"ElTabs",version:"2.3.0",ref:"https://element-plus.org/en-US/component/tabs.html#attributes",type:"Attribute"},ee(()=>!!e.activeName)),ke(()=>e.activeName,v=>c(v)),ke(()=>e.modelValue,v=>c(v)),ke(u,async()=>{var v;await He(),(v=o.value)==null||v.scrollToActiveTab()}),gt(rf,{props:e,currentName:u,registerPane:y=>a[y.uid]=y,unregisterPane:y=>delete a[y]}),r({currentName:u}),()=>{const v=e.editable||e.addable?le("span",{class:g.e("new-tab"),tabindex:"0",onClick:h,onKeydown:b=>{b.code===Ot.enter&&h()}},[le(Me,{class:g.is("icon-plus")},{default:()=>[le(vp,null,null)]})]):null,m=le("div",{class:[g.e("header"),g.is(e.tabPosition)]},[v,le(Wj,{ref:o,currentName:u.value,editable:e.editable,type:e.type,panes:Object.values(a),stretch:e.stretch,onTabClick:f,onTabRemove:s},null)]),y=le("div",{class:g.e("content")},[xe(n,"default")]);return le("div",{class:[g.b(),g.m(e.tabPosition),{[g.m("card")]:e.type==="card",[g.m("border-card")]:e.type==="border-card"}]},[...e.tabPosition!=="bottom"?[m,y]:[y,m]])}}});const Gj=Fe({label:{type:String,default:""},name:{type:[String,Number]},closable:Boolean,disabled:Boolean,lazy:Boolean}),qj=["id","aria-hidden","aria-labelledby"],Zj={name:"ElTabPane"},Jj=Ee({...Zj,props:Gj,setup(e){const t=e,n="ElTabPane",r=xt(),i=Ho(),l=Ke(rf);l||Tn(n,"usage: ");const g=Ie("tab-pane"),o=oe(),a=ee(()=>t.closable||l.props.closable),u=bv(()=>{var h;return l.currentName.value===((h=t.name)!=null?h:o.value)}),d=oe(u.value),c=ee(()=>{var h;return(h=t.name)!=null?h:o.value}),f=bv(()=>!t.lazy||d.value||u.value);ke(u,h=>{h&&(d.value=!0)});const s=vt({uid:r.uid,slots:i,props:t,paneName:c,active:u,index:o,isClosable:a});return rt(()=>{l.registerPane(s)}),gi(()=>{l.unregisterPane(s.uid)}),(h,v)=>C(f)?Et((z(),se("div",{key:0,id:`pane-${C(c)}`,class:de(C(g).b()),role:"tabpanel","aria-hidden":!C(u),"aria-labelledby":`tab-${C(c)}`},[xe(h.$slots,"default")],10,qj)),[[Ht,C(u)]]):ye("v-if",!0)}});var Ow=Le(Jj,[["__file","/home/runner/work/element-plus/element-plus/packages/components/tabs/src/tab-pane.vue"]]);const Qj=kt(Kj,{TabPane:Ow}),Wf=wr(Ow),eV="ElUpload";class tV extends Error{constructor(t,n,r,i){super(t),this.name="UploadAjaxError",this.status=n,this.method=r,this.url=i}}function I0(e,t,n){let r;return n.response?r=`${n.response.error||n.response}`:n.responseText?r=`${n.responseText}`:r=`fail to ${t.method} ${e} ${n.status}`,new tV(r,n.status,t.method,e)}function nV(e){const t=e.responseText||e.response;if(!t)return t;try{return JSON.parse(t)}catch{return t}}const rV=e=>{typeof XMLHttpRequest>"u"&&Tn(eV,"XMLHttpRequest is undefined");const t=new XMLHttpRequest,n=e.action;t.upload&&t.upload.addEventListener("progress",l=>{const g=l;g.percent=l.total>0?l.loaded/l.total*100:0,e.onProgress(g)});const r=new FormData;if(e.data)for(const[l,g]of Object.entries(e.data))Array.isArray(g)?r.append(l,...g):r.append(l,g);r.append(e.filename,e.file,e.file.name),t.addEventListener("error",()=>{e.onError(I0(n,e,t))}),t.addEventListener("load",()=>{if(t.status<200||t.status>=300)return e.onError(I0(n,e,t));e.onSuccess(nV(t))}),t.open(e.method,n,!0),e.withCredentials&&"withCredentials"in t&&(t.withCredentials=!0);const i=e.headers||{};if(i instanceof Headers)i.forEach((l,g)=>t.setRequestHeader(g,l));else for(const[l,g]of Object.entries(i))Mr(g)||t.setRequestHeader(l,String(g));return t.send(r),t},Aw=["text","picture","picture-card"];let iV=1;const Pw=()=>Date.now()+iV++,Iw=Fe({action:{type:String,default:"#"},headers:{type:Pe(Object)},method:{type:String,default:"post"},data:{type:Object,default:()=>sn({})},multiple:{type:Boolean,default:!1},name:{type:String,default:"file"},drag:{type:Boolean,default:!1},withCredentials:Boolean,showFileList:{type:Boolean,default:!0},accept:{type:String,default:""},type:{type:String,default:"select"},fileList:{type:Pe(Array),default:()=>sn([])},autoUpload:{type:Boolean,default:!0},listType:{type:String,values:Aw,default:"text"},httpRequest:{type:Pe(Function),default:rV},disabled:Boolean,limit:Number}),aV=Fe({...Iw,beforeUpload:{type:Pe(Function),default:ct},beforeRemove:{type:Pe(Function)},onRemove:{type:Pe(Function),default:ct},onChange:{type:Pe(Function),default:ct},onPreview:{type:Pe(Function),default:ct},onSuccess:{type:Pe(Function),default:ct},onProgress:{type:Pe(Function),default:ct},onError:{type:Pe(Function),default:ct},onExceed:{type:Pe(Function),default:ct}}),oV=Fe({files:{type:Pe(Array),default:()=>sn([])},disabled:{type:Boolean,default:!1},handlePreview:{type:Pe(Function),default:ct},listType:{type:String,values:Aw,default:"text"}}),sV={remove:e=>!!e},lV=["onKeydown"],uV=["src"],cV=["onClick"],fV=["onClick"],dV=["onClick"],hV={name:"ElUploadList"},pV=Ee({...hV,props:oV,emits:sV,setup(e,{emit:t}){const{t:n}=yn(),r=Ie("upload"),i=Ie("icon"),l=Ie("list"),g=oe(!1),o=a=>{t("remove",a)};return(a,u)=>(z(),be(qx,{tag:"ul",class:de([C(r).b("list"),C(r).bm("list",a.listType),C(r).is("disabled",a.disabled)]),name:C(l).b()},{default:he(()=>[(z(!0),se(Ne,null,Nt(a.files,d=>(z(),se("li",{key:d.uid||d.name,class:de([C(r).be("list","item"),C(r).is(d.status),{focusing:g.value}]),tabindex:"0",onKeydown:$t(c=>!a.disabled&&o(d),["delete"]),onFocus:u[0]||(u[0]=c=>g.value=!0),onBlur:u[1]||(u[1]=c=>g.value=!1),onClick:u[2]||(u[2]=c=>g.value=!1)},[xe(a.$slots,"default",{file:d},()=>[a.listType==="picture"||d.status!=="uploading"&&a.listType==="picture-card"?(z(),se("img",{key:0,class:de(C(r).be("list","item-thumbnail")),src:d.url,alt:""},null,10,uV)):ye("v-if",!0),d.status==="uploading"||a.listType!=="picture-card"?(z(),se("div",{key:1,class:de(C(r).be("list","item-info"))},[ne("a",{class:de(C(r).be("list","item-name")),onClick:tt(c=>a.handlePreview(d),["prevent"])},[le(C(Me),{class:de(C(i).m("document"))},{default:he(()=>[le(C(S_))]),_:1},8,["class"]),ne("span",{class:de(C(r).be("list","item-file-name"))},Ae(d.name),3)],10,cV),d.status==="uploading"?(z(),be(C(Ew),{key:0,type:a.listType==="picture-card"?"circle":"line","stroke-width":a.listType==="picture-card"?6:2,percentage:Number(d.percentage),style:ze(a.listType==="picture-card"?"":"margin-top: 0.5rem")},null,8,["type","stroke-width","percentage","style"])):ye("v-if",!0)],2)):ye("v-if",!0),ne("label",{class:de(C(r).be("list","item-status-label"))},[a.listType==="text"?(z(),be(C(Me),{key:0,class:de([C(i).m("upload-success"),C(i).m("circle-check")])},{default:he(()=>[le(C(Ll))]),_:1},8,["class"])):["picture-card","picture"].includes(a.listType)?(z(),be(C(Me),{key:1,class:de([C(i).m("upload-success"),C(i).m("check")])},{default:he(()=>[le(C(__))]),_:1},8,["class"])):ye("v-if",!0)],2),a.disabled?ye("v-if",!0):(z(),be(C(Me),{key:2,class:de(C(i).m("close")),onClick:c=>o(d)},{default:he(()=>[le(C($r))]),_:2},1032,["class","onClick"])),ye(" Due to close btn only appears when li gets focused disappears after li gets blurred, thus keyboard navigation can never reach close btn"),ye(" This is a bug which needs to be fixed "),ye(" TODO: Fix the incorrect navigation interaction "),a.disabled?ye("v-if",!0):(z(),se("i",{key:3,class:de(C(i).m("close-tip"))},Ae(C(n)("el.upload.deleteTip")),3)),a.listType==="picture-card"?(z(),se("span",{key:4,class:de(C(r).be("list","item-actions"))},[ne("span",{class:de(C(r).be("list","item-preview")),onClick:c=>a.handlePreview(d)},[le(C(Me),{class:de(C(i).m("zoom-in"))},{default:he(()=>[le(C(A_))]),_:1},8,["class"])],10,fV),a.disabled?ye("v-if",!0):(z(),se("span",{key:0,class:de(C(r).be("list","item-delete")),onClick:c=>o(d)},[le(C(Me),{class:de(C(i).m("delete"))},{default:he(()=>[le(C(Rl))]),_:1},8,["class"])],10,dV))],2)):ye("v-if",!0)])],42,lV))),128)),xe(a.$slots,"append")]),_:3},8,["class","name"]))}});var M0=Le(pV,[["__file","/home/runner/work/element-plus/element-plus/packages/components/upload/src/upload-list.vue"]]);const mV=Fe({disabled:{type:Boolean,default:!1}}),gV={file:e=>Ve(e)},vV=["onDrop","onDragover"],yV={name:"ElUploadDrag"},bV=Ee({...yV,props:mV,emits:gV,setup(e,{emit:t}){const n=e,r="ElUploadDrag",i=Ke(A1);i||Tn(r,"usage: ");const l=Ie("upload"),g=oe(!1),o=u=>{if(n.disabled)return;g.value=!1;const d=Array.from(u.dataTransfer.files),c=i.accept.value;if(!c){t("file",d);return}const f=d.filter(s=>{const{type:h,name:v}=s,m=v.includes(".")?`.${v.split(".").pop()}`:"",y=h.replace(/\/.*$/,"");return c.split(",").map(b=>b.trim()).filter(b=>b).some(b=>b.startsWith(".")?m===b:/\/\*$/.test(b)?y===b.replace(/\/\*$/,""):/^[^/]+\/[^/]+$/.test(b)?h===b:!1)});t("file",f)},a=()=>{n.disabled||(g.value=!0)};return(u,d)=>(z(),se("div",{class:de([C(l).b("dragger"),C(l).is("dragover",g.value)]),onDrop:tt(o,["prevent"]),onDragover:tt(a,["prevent"]),onDragleave:d[0]||(d[0]=tt(c=>g.value=!1,["prevent"]))},[xe(u.$slots,"default")],42,vV))}});var _V=Le(bV,[["__file","/home/runner/work/element-plus/element-plus/packages/components/upload/src/upload-dragger.vue"]]);const wV=Fe({...Iw,beforeUpload:{type:Pe(Function),default:ct},onRemove:{type:Pe(Function),default:ct},onStart:{type:Pe(Function),default:ct},onSuccess:{type:Pe(Function),default:ct},onProgress:{type:Pe(Function),default:ct},onError:{type:Pe(Function),default:ct},onExceed:{type:Pe(Function),default:ct}}),CV=["onKeydown"],SV=["name","multiple","accept"],xV={name:"ElUploadContent",inheritAttrs:!1},kV=Ee({...xV,props:wV,setup(e,{expose:t}){const n=e,r=Ie("upload"),i=gn({}),l=gn(),g=s=>{if(s.length===0)return;const{autoUpload:h,limit:v,fileList:m,multiple:y,onStart:b,onExceed:_}=n;if(v&&m.length+s.length>v){_(s,m);return}y||(s=s.slice(0,1));for(const w of s){const S=w;S.uid=Pw(),b(S),h&&o(S)}},o=async s=>{if(l.value.value="",!n.beforeUpload)return a(s);let h;try{h=await n.beforeUpload(s)}catch{h=!1}if(h===!1){n.onRemove(s);return}let v=s;h instanceof Blob&&(h instanceof File?v=h:v=new File([h],s.name,{type:s.type})),a(Object.assign(v,{uid:s.uid}))},a=s=>{const{headers:h,data:v,method:m,withCredentials:y,name:b,action:_,onProgress:w,onSuccess:S,onError:x,httpRequest:k}=n,{uid:E}=s,I={headers:h||{},withCredentials:y,file:s,data:v,method:m,filename:b,action:_,onProgress:N=>{w(N,s)},onSuccess:N=>{S(N,s),delete i.value[E]},onError:N=>{x(N,s),delete i.value[E]}},D=k(I);i.value[E]=D,D instanceof Promise&&D.then(I.onSuccess,I.onError)},u=s=>{const h=s.target.files;!h||g(Array.from(h))},d=()=>{n.disabled||(l.value.value="",l.value.click())},c=()=>{d()};return t({abort:s=>{AM(i.value).filter(s?([v])=>String(s.uid)===v:()=>!0).forEach(([v,m])=>{m instanceof XMLHttpRequest&&m.abort(),delete i.value[v]})},upload:o}),(s,h)=>(z(),se("div",{class:de([C(r).b(),C(r).m(s.listType),C(r).is("drag",s.drag)]),tabindex:"0",onClick:d,onKeydown:$t(tt(c,["self"]),["enter","space"])},[s.drag?(z(),be(_V,{key:0,disabled:s.disabled,onFile:g},{default:he(()=>[xe(s.$slots,"default")]),_:3},8,["disabled"])):xe(s.$slots,"default",{key:1}),ne("input",{ref_key:"inputRef",ref:l,class:de(C(r).e("input")),name:s.name,multiple:s.multiple,accept:s.accept,type:"file",onChange:u,onClick:h[0]||(h[0]=tt(()=>{},["stop"]))},null,42,SV)],42,CV))}});var L0=Le(kV,[["__file","/home/runner/work/element-plus/element-plus/packages/components/upload/src/upload-content.vue"]]);const R0="ElUpload",TV=e=>{var t;(t=e.url)!=null&&t.startsWith("blob:")&&URL.revokeObjectURL(e.url)},EV=(e,t)=>{const n=kM(e,"fileList",void 0,{passive:!0}),r=f=>n.value.find(s=>s.uid===f.uid);function i(f){var s;(s=t.value)==null||s.abort(f)}function l(f=["ready","uploading","success","fail"]){n.value=n.value.filter(s=>!f.includes(s.status))}const g=(f,s)=>{const h=r(s);!h||(console.error(f),h.status="fail",n.value.splice(n.value.indexOf(h),1),e.onError(f,h,n.value),e.onChange(h,n.value))},o=(f,s)=>{const h=r(s);!h||(e.onProgress(f,h,n.value),h.status="uploading",h.percentage=Math.round(f.percent))},a=(f,s)=>{const h=r(s);!h||(h.status="success",h.response=f,e.onSuccess(f,h,n.value),e.onChange(h,n.value))},u=f=>{const s={name:f.name,percentage:0,status:"ready",size:f.size,raw:f,uid:f.uid};if(e.listType==="picture-card"||e.listType==="picture")try{s.url=URL.createObjectURL(f)}catch(h){h.message,e.onError(h,s,n.value)}n.value=[...n.value,s],e.onChange(s,n.value)},d=async f=>{const s=f instanceof File?r(f):f;s||Tn(R0,"file to be removed not found");const h=v=>{i(v);const m=n.value;m.splice(m.indexOf(v),1),e.onRemove(v,m),TV(v)};e.beforeRemove?await e.beforeRemove(s,n.value)!==!1&&h(s):h(s)};function c(){n.value.filter(({status:f})=>f==="ready").forEach(({raw:f})=>{var s;return f&&((s=t.value)==null?void 0:s.upload(f))})}return ke(()=>e.listType,f=>{f!=="picture-card"&&f!=="picture"||(n.value=n.value.map(s=>{const{raw:h,url:v}=s;if(!v&&h)try{s.url=URL.createObjectURL(h)}catch(m){e.onError(m,s,n.value)}return s}))}),ke(n,f=>{for(const s of f)s.uid||(s.uid=Pw()),s.status||(s.status="success")},{immediate:!0,deep:!0}),{uploadFiles:n,abort:i,clearFiles:l,handleError:g,handleProgress:o,handleStart:u,handleSuccess:a,handleRemove:d,submit:c}},OV={name:"ElUpload"},AV=Ee({...OV,props:aV,setup(e,{expose:t}){const n=e,r=Ho(),i=Ja(),l=gn(),{abort:g,submit:o,clearFiles:a,uploadFiles:u,handleStart:d,handleError:c,handleRemove:f,handleSuccess:s,handleProgress:h}=EV(n,l),v=ee(()=>n.listType==="picture-card"),m=ee(()=>({...n,fileList:u.value,onStart:d,onProgress:h,onSuccess:s,onError:c,onRemove:f}));return Xt(()=>{u.value.forEach(({url:y})=>{y!=null&&y.startsWith("blob:")&&URL.revokeObjectURL(y)})}),gt(A1,{accept:Rn(n,"accept")}),t({abort:g,submit:o,clearFiles:a,handleStart:d,handleRemove:f}),(y,b)=>(z(),se("div",null,[C(v)&&y.showFileList?(z(),be(M0,{key:0,disabled:C(i),"list-type":y.listType,files:C(u),"handle-preview":y.onPreview,onRemove:C(f)},sc({append:he(()=>[le(L0,In({ref_key:"uploadRef",ref:l},C(m)),{default:he(()=>[C(r).trigger?xe(y.$slots,"trigger",{key:0}):ye("v-if",!0),!C(r).trigger&&C(r).default?xe(y.$slots,"default",{key:1}):ye("v-if",!0)]),_:3},16)]),_:2},[y.$slots.file?{name:"default",fn:he(({file:_})=>[xe(y.$slots,"file",{file:_})])}:void 0]),1032,["disabled","list-type","files","handle-preview","onRemove"])):ye("v-if",!0),!C(v)||C(v)&&!y.showFileList?(z(),be(L0,In({key:1,ref_key:"uploadRef",ref:l},C(m)),{default:he(()=>[C(r).trigger?xe(y.$slots,"trigger",{key:0}):ye("v-if",!0),!C(r).trigger&&C(r).default?xe(y.$slots,"default",{key:1}):ye("v-if",!0)]),_:3},16)):ye("v-if",!0),y.$slots.trigger?xe(y.$slots,"default",{key:2}):ye("v-if",!0),xe(y.$slots,"tip"),!C(v)&&y.showFileList?(z(),be(M0,{key:3,disabled:C(i),"list-type":y.listType,files:C(u),"handle-preview":y.onPreview,onRemove:C(f)},sc({_:2},[y.$slots.file?{name:"default",fn:he(({file:_})=>[xe(y.$slots,"file",{file:_})])}:void 0]),1032,["disabled","list-type","files","handle-preview","onRemove"])):ye("v-if",!0)]))}});var PV=Le(AV,[["__file","/home/runner/work/element-plus/element-plus/packages/components/upload/src/upload.vue"]]);const Jp=kt(PV),Mw=["success","info","warning","error"],On=sn({customClass:"",center:!1,dangerouslyUseHTMLString:!1,duration:3e3,icon:void 0,id:"",message:"",onClose:void 0,showClose:!1,type:"info",offset:16,zIndex:0,grouping:!1,repeatNum:1,appendTo:dt?document.body:void 0}),IV=Fe({customClass:{type:String,default:On.customClass},center:{type:Boolean,default:On.center},dangerouslyUseHTMLString:{type:Boolean,default:On.dangerouslyUseHTMLString},duration:{type:Number,default:On.duration},icon:{type:Wt,default:On.icon},id:{type:String,default:On.id},message:{type:Pe([String,Object,Function]),default:On.message},onClose:{type:Pe(Function),required:!1},showClose:{type:Boolean,default:On.showClose},type:{type:String,values:Mw,default:On.type},offset:{type:Number,default:On.offset},zIndex:{type:Number,default:On.zIndex},grouping:{type:Boolean,default:On.grouping},repeatNum:{type:Number,default:On.repeatNum}}),MV={destroy:()=>!0},Ir=lb([]),LV=e=>{const t=Ir.findIndex(i=>i.id===e),n=Ir[t];let r;return t>0&&(r=Ir[t-1]),{current:n,prev:r}},RV=e=>{const{prev:t}=LV(e);return t?t.vm.exposed.bottom.value:0},DV=["id"],$V=["innerHTML"],BV={name:"ElMessage"},FV=Ee({...BV,props:IV,emits:MV,setup(e,{expose:t}){const n=e,{Close:r}=g1,i=Ie("message"),l=oe(),g=oe(!1),o=oe(0);let a;const u=ee(()=>n.type?n.type==="error"?"danger":n.type:"info"),d=ee(()=>{const w=n.type;return{[i.bm("icon",w)]:w&&vc[w]}}),c=ee(()=>n.icon||vc[n.type]||""),f=ee(()=>RV(n.id)),s=ee(()=>n.offset+f.value),h=ee(()=>o.value+s.value),v=ee(()=>({top:`${s.value}px`,zIndex:n.zIndex}));function m(){n.duration!==0&&({stop:a}=il(()=>{b()},n.duration))}function y(){a==null||a()}function b(){g.value=!1}function _({code:w}){w===Ot.esc&&b()}return rt(()=>{m(),g.value=!0}),ke(()=>n.repeatNum,()=>{y(),m()}),St(document,"keydown",_),Nr(l,()=>{o.value=l.value.getBoundingClientRect().height}),t({visible:g,bottom:h,close:b}),(w,S)=>(z(),be(vn,{name:C(i).b("fade"),onBeforeLeave:w.onClose,onAfterLeave:S[0]||(S[0]=x=>w.$emit("destroy")),persisted:""},{default:he(()=>[Et(ne("div",{id:w.id,ref_key:"messageRef",ref:l,class:de([C(i).b(),{[C(i).m(w.type)]:w.type&&!w.icon},C(i).is("center",w.center),C(i).is("closable",w.showClose),w.customClass]),style:ze(C(v)),role:"alert",onMouseenter:y,onMouseleave:m},[w.repeatNum>1?(z(),be(C(WD),{key:0,value:w.repeatNum,type:C(u),class:de(C(i).e("badge"))},null,8,["value","type","class"])):ye("v-if",!0),C(c)?(z(),be(C(Me),{key:1,class:de([C(i).e("icon"),C(d)])},{default:he(()=>[(z(),be(Ct(C(c))))]),_:1},8,["class"])):ye("v-if",!0),xe(w.$slots,"default",{},()=>[w.dangerouslyUseHTMLString?(z(),se(Ne,{key:1},[ye(" Caution here, message could've been compromised, never use user's input as message "),ne("p",{class:de(C(i).e("content")),innerHTML:w.message},null,10,$V)],2112)):(z(),se("p",{key:0,class:de(C(i).e("content"))},Ae(w.message),3))]),w.showClose?(z(),be(C(Me),{key:2,class:de(C(i).e("closeBtn")),onClick:tt(b,["stop"])},{default:he(()=>[le(C(r))]),_:1},8,["class","onClick"])):ye("v-if",!0)],46,DV),[[Ht,g.value]])]),_:3},8,["name","onBeforeLeave"]))}});var zV=Le(FV,[["__file","/home/runner/work/element-plus/element-plus/packages/components/message/src/message.vue"]]);let NV=1;const Lw=e=>{const t=!e||Xe(e)||Yi(e)||Ue(e)?{message:e}:e,n={...On,...t};if(!n.appendTo)n.appendTo=document.body;else if(Xe(n.appendTo)){let r=document.querySelector(n.appendTo);$a(r)||(r=document.body),n.appendTo=r}return n},jV=e=>{const t=Ir.indexOf(e);if(t===-1)return;Ir.splice(t,1);const{handler:n}=e;n.close()},VV=({appendTo:e,...t},n)=>{const{nextZIndex:r}=Bl(),i=`message_${NV++}`,l=t.onClose,g=document.createElement("div"),o={...t,zIndex:r()+t.zIndex,id:i,onClose:()=>{l==null||l(),jV(c)},onDestroy:()=>{fc(null,g)}},a=le(zV,o,Ue(o.message)||Yi(o.message)?{default:Ue(o.message)?o.message:()=>o.message}:null);a.appContext=n||No._context,fc(a,g),e.appendChild(g.firstElementChild);const u=a.component,c={id:i,vnode:a,vm:u,handler:{close:()=>{u.exposed.visible.value=!1}},props:a.component.props};return c},No=(e={},t)=>{if(!dt)return{close:()=>{}};if(ft(Jd.max)&&Ir.length>=Jd.max)return{close:()=>{}};const n=Lw(e);if(n.grouping&&Ir.length){const i=Ir.find(({vnode:l})=>{var g;return((g=l.props)==null?void 0:g.message)===n.message});if(i)return i.props.repeatNum+=1,i.props.type=n.type,i.handler}const r=VV(n,t);return Ir.push(r),r.handler};Mw.forEach(e=>{No[e]=(t={},n)=>{const r=Lw(t);return No({...r,type:e},n)}});function UV(e){for(const t of Ir)(!e||e===t.props.type)&&t.handler.close()}No.closeAll=UV;No._context=null;const qi=FM(No,"$message"),HV=Ee({name:"ElMessageBox",directives:{TrapFocus:y$},components:{ElButton:at,ElFocusTrap:Wp,ElInput:Qa,ElOverlay:pw,ElIcon:Me,...g1},inheritAttrs:!1,props:{buttonSize:{type:String,validator:nf},modal:{type:Boolean,default:!0},lockScroll:{type:Boolean,default:!0},showClose:{type:Boolean,default:!0},closeOnClickModal:{type:Boolean,default:!0},closeOnPressEscape:{type:Boolean,default:!0},closeOnHashChange:{type:Boolean,default:!0},center:Boolean,draggable:Boolean,roundButton:{default:!1,type:Boolean},container:{type:String,default:"body"},boxType:{type:String,default:""}},emits:["vanish","action"],setup(e,{emit:t}){const{t:n}=yn(),r=Ie("message-box"),i=oe(!1),{nextZIndex:l}=Bl(),g=vt({autofocus:!0,beforeClose:null,callback:null,cancelButtonText:"",cancelButtonClass:"",confirmButtonText:"",confirmButtonClass:"",customClass:"",customStyle:{},dangerouslyUseHTMLString:!1,distinguishCancelAndClose:!1,icon:"",inputPattern:null,inputPlaceholder:"",inputType:"text",inputValue:null,inputValidator:null,inputErrorMessage:"",message:null,modalFade:!0,modalClass:"",showCancelButton:!1,showConfirmButton:!0,type:"",title:void 0,showInput:!1,action:"",confirmButtonLoading:!1,cancelButtonLoading:!1,confirmButtonDisabled:!1,editorErrorMessage:"",validateError:!1,zIndex:l()}),o=ee(()=>{const F=g.type;return{[r.bm("icon",F)]:F&&vc[F]}}),a=Gi(),u=Gi(),d=sr(ee(()=>e.buttonSize),{prop:!0,form:!0,formItem:!0}),c=ee(()=>g.icon||vc[g.type]||""),f=ee(()=>!!g.message),s=oe(),h=oe(),v=oe(),m=oe(),y=oe(),b=ee(()=>g.confirmButtonClass);ke(()=>g.inputValue,async F=>{await He(),e.boxType==="prompt"&&F!==null&&I()},{immediate:!0}),ke(()=>i.value,F=>{var O,V;F&&(e.boxType!=="prompt"&&(g.autofocus?v.value=(V=(O=y.value)==null?void 0:O.$el)!=null?V:s.value:v.value=s.value),g.zIndex=l()),e.boxType==="prompt"&&(F?He().then(()=>{var re;m.value&&m.value.$el&&(g.autofocus?v.value=(re=D())!=null?re:s.value:v.value=s.value)}):(g.editorErrorMessage="",g.validateError=!1))});const _=ee(()=>e.draggable);M1(s,h,_),rt(async()=>{await He(),e.closeOnHashChange&&window.addEventListener("hashchange",w)}),Xt(()=>{e.closeOnHashChange&&window.removeEventListener("hashchange",w)});function w(){!i.value||(i.value=!1,He(()=>{g.action&&t("action",g.action)}))}const S=()=>{e.closeOnClickModal&&E(g.distinguishCancelAndClose?"close":"cancel")},x=$p(S),k=F=>{if(g.inputType!=="textarea")return F.preventDefault(),E("confirm")},E=F=>{var O;e.boxType==="prompt"&&F==="confirm"&&!I()||(g.action=F,g.beforeClose?(O=g.beforeClose)==null||O.call(g,F,g,w):w())},I=()=>{if(e.boxType==="prompt"){const F=g.inputPattern;if(F&&!F.test(g.inputValue||""))return g.editorErrorMessage=g.inputErrorMessage||n("el.messagebox.error"),g.validateError=!0,!1;const O=g.inputValidator;if(typeof O=="function"){const V=O(g.inputValue);if(V===!1)return g.editorErrorMessage=g.inputErrorMessage||n("el.messagebox.error"),g.validateError=!0,!1;if(typeof V=="string")return g.editorErrorMessage=V,g.validateError=!0,!1}}return g.editorErrorMessage="",g.validateError=!1,!0},D=()=>{const F=m.value.$refs;return F.input||F.textarea},N=()=>{E("close")},L=()=>{e.closeOnPressEscape&&N()};return e.lockScroll&&L1(i),oL(i),{...rr(g),ns:r,overlayEvent:x,visible:i,hasMessage:f,typeClass:o,contentId:a,inputId:u,btnSize:d,iconComponent:c,confirmButtonClasses:b,rootRef:s,focusStartRef:v,headerRef:h,inputRef:m,confirmRef:y,doClose:w,handleClose:N,onCloseRequested:L,handleWrapperClick:S,handleInputEnter:k,handleAction:E,t:n}}}),WV=["aria-label","aria-describedby"],YV=["aria-label"],XV=["id"];function KV(e,t,n,r,i,l){const g=mt("el-icon"),o=mt("close"),a=mt("el-input"),u=mt("el-button"),d=mt("el-focus-trap"),c=mt("el-overlay");return z(),be(vn,{name:"fade-in-linear",onAfterLeave:t[11]||(t[11]=f=>e.$emit("vanish")),persisted:""},{default:he(()=>[Et(le(c,{"z-index":e.zIndex,"overlay-class":[e.ns.is("message-box"),e.modalClass],mask:e.modal},{default:he(()=>[ne("div",{role:"dialog","aria-label":e.title,"aria-modal":"true","aria-describedby":e.showInput?void 0:e.contentId,class:de(`${e.ns.namespace.value}-overlay-message-box`),onClick:t[8]||(t[8]=(...f)=>e.overlayEvent.onClick&&e.overlayEvent.onClick(...f)),onMousedown:t[9]||(t[9]=(...f)=>e.overlayEvent.onMousedown&&e.overlayEvent.onMousedown(...f)),onMouseup:t[10]||(t[10]=(...f)=>e.overlayEvent.onMouseup&&e.overlayEvent.onMouseup(...f))},[le(d,{loop:"",trapped:e.visible,"focus-trap-el":e.rootRef,"focus-start-el":e.focusStartRef,onReleaseRequested:e.onCloseRequested},{default:he(()=>[ne("div",{ref:"rootRef",class:de([e.ns.b(),e.customClass,e.ns.is("draggable",e.draggable),{[e.ns.m("center")]:e.center}]),style:ze(e.customStyle),tabindex:"-1",onClick:t[7]||(t[7]=tt(()=>{},["stop"]))},[e.title!==null&&e.title!==void 0?(z(),se("div",{key:0,ref:"headerRef",class:de(e.ns.e("header"))},[ne("div",{class:de(e.ns.e("title"))},[e.iconComponent&&e.center?(z(),be(g,{key:0,class:de([e.ns.e("status"),e.typeClass])},{default:he(()=>[(z(),be(Ct(e.iconComponent)))]),_:1},8,["class"])):ye("v-if",!0),ne("span",null,Ae(e.title),1)],2),e.showClose?(z(),se("button",{key:0,type:"button",class:de(e.ns.e("headerbtn")),"aria-label":e.t("el.messagebox.close"),onClick:t[0]||(t[0]=f=>e.handleAction(e.distinguishCancelAndClose?"close":"cancel")),onKeydown:t[1]||(t[1]=$t(tt(f=>e.handleAction(e.distinguishCancelAndClose?"close":"cancel"),["prevent"]),["enter"]))},[le(g,{class:de(e.ns.e("close"))},{default:he(()=>[le(o)]),_:1},8,["class"])],42,YV)):ye("v-if",!0)],2)):ye("v-if",!0),ne("div",{id:e.contentId,class:de(e.ns.e("content"))},[ne("div",{class:de(e.ns.e("container"))},[e.iconComponent&&!e.center&&e.hasMessage?(z(),be(g,{key:0,class:de([e.ns.e("status"),e.typeClass])},{default:he(()=>[(z(),be(Ct(e.iconComponent)))]),_:1},8,["class"])):ye("v-if",!0),e.hasMessage?(z(),se("div",{key:1,class:de(e.ns.e("message"))},[xe(e.$slots,"default",{},()=>[e.dangerouslyUseHTMLString?(z(),be(Ct(e.showInput?"label":"p"),{key:1,for:e.showInput?e.inputId:void 0,innerHTML:e.message},null,8,["for","innerHTML"])):(z(),be(Ct(e.showInput?"label":"p"),{key:0,for:e.showInput?e.inputId:void 0},{default:he(()=>[je(Ae(e.dangerouslyUseHTMLString?"":e.message),1)]),_:1},8,["for"]))])],2)):ye("v-if",!0)],2),Et(ne("div",{class:de(e.ns.e("input"))},[le(a,{id:e.inputId,ref:"inputRef",modelValue:e.inputValue,"onUpdate:modelValue":t[2]||(t[2]=f=>e.inputValue=f),type:e.inputType,placeholder:e.inputPlaceholder,"aria-invalid":e.validateError,class:de({invalid:e.validateError}),onKeydown:$t(e.handleInputEnter,["enter"])},null,8,["id","modelValue","type","placeholder","aria-invalid","class","onKeydown"]),ne("div",{class:de(e.ns.e("errormsg")),style:ze({visibility:e.editorErrorMessage?"visible":"hidden"})},Ae(e.editorErrorMessage),7)],2),[[Ht,e.showInput]])],10,XV),ne("div",{class:de(e.ns.e("btns"))},[e.showCancelButton?(z(),be(u,{key:0,loading:e.cancelButtonLoading,class:de([e.cancelButtonClass]),round:e.roundButton,size:e.btnSize,onClick:t[3]||(t[3]=f=>e.handleAction("cancel")),onKeydown:t[4]||(t[4]=$t(tt(f=>e.handleAction("cancel"),["prevent"]),["enter"]))},{default:he(()=>[je(Ae(e.cancelButtonText||e.t("el.messagebox.cancel")),1)]),_:1},8,["loading","class","round","size"])):ye("v-if",!0),Et(le(u,{ref:"confirmRef",type:"primary",loading:e.confirmButtonLoading,class:de([e.confirmButtonClasses]),round:e.roundButton,disabled:e.confirmButtonDisabled,size:e.btnSize,onClick:t[5]||(t[5]=f=>e.handleAction("confirm")),onKeydown:t[6]||(t[6]=$t(tt(f=>e.handleAction("confirm"),["prevent"]),["enter"]))},{default:he(()=>[je(Ae(e.confirmButtonText||e.t("el.messagebox.confirm")),1)]),_:1},8,["loading","class","round","disabled","size"]),[[Ht,e.showConfirmButton]])],2)],6)]),_:3},8,["trapped","focus-trap-el","focus-start-el","onReleaseRequested"])],42,WV)]),_:3},8,["z-index","overlay-class","mask"]),[[Ht,e.visible]])]),_:3})}var GV=Le(HV,[["render",KV],["__file","/home/runner/work/element-plus/element-plus/packages/components/message-box/src/index.vue"]]);const fl=new Map,qV=(e,t,n=null)=>{const r=le(GV,e,Ue(e.message)||Yi(e.message)?{default:Ue(e.message)?e.message:()=>e.message}:null);return r.appContext=n,fc(r,t),document.body.appendChild(t.firstElementChild),r.component},ZV=()=>document.createElement("div"),JV=(e,t)=>{const n=ZV();e.onVanish=()=>{fc(null,n),fl.delete(i)},e.onAction=l=>{const g=fl.get(i);let o;e.showInput?o={value:i.inputValue,action:l}:o=l,e.callback?e.callback(o,r.proxy):l==="cancel"||l==="close"?e.distinguishCancelAndClose&&l!=="cancel"?g.reject("close"):g.reject("cancel"):g.resolve(o)};const r=qV(e,n,t),i=r.proxy;for(const l in e)nt(e,l)&&!nt(i.$props,l)&&(i[l]=e[l]);return i.visible=!0,i};function qo(e,t=null){if(!dt)return Promise.reject();let n;return Xe(e)||Yi(e)?e={message:e}:n=e.callback,new Promise((r,i)=>{const l=JV(e,t!=null?t:qo._context);fl.set(l,{options:e,callback:n,resolve:r,reject:i})})}const QV=["alert","confirm","prompt"],e7={alert:{closeOnPressEscape:!1,closeOnClickModal:!1},confirm:{showCancelButton:!0},prompt:{showCancelButton:!0,showInput:!0}};QV.forEach(e=>{qo[e]=t7(e)});function t7(e){return(t,n,r,i)=>{let l="";return ot(n)?(r=n,l=""):pr(n)?l="":l=n,qo(Object.assign({title:l,message:t,type:"",...e7[e]},r,{boxType:e}),i)}}qo.close=()=>{fl.forEach((e,t)=>{t.doClose()}),fl.clear()};qo._context=null;const Ai=qo;Ai.install=e=>{Ai._context=e._context,e.config.globalProperties.$msgbox=Ai,e.config.globalProperties.$messageBox=Ai,e.config.globalProperties.$alert=Ai.alert,e.config.globalProperties.$confirm=Ai.confirm,e.config.globalProperties.$prompt=Ai.prompt};const Rw=Ai;const Rt=Wo("options",()=>{const e=fn("options",oe({colorMode:Mv({emitAuto:!0})})),t=fn("pageSize",25),n=fn("pageless","Disabled"),r=fn("allowLargerParams","Disabled"),i=fn("alsoRequestAvi","Disabled"),l=fn("keepImageGenOnDisconnect","Disabled"),g=fn("autoCarousel","Enabled"),o=fn("useBeta","Disabled"),a=fn("imageDownloadType","PNG"),u=fn("imageResizeMode","NoScale"),d=fn("baseURL","");return e.value.colorMode=Mv({emitAuto:!0,initialValue:e.value.colorMode}),{options:e,pageSize:t,pageless:n,allowLargerParams:r,alsoRequestAvi:i,keepImageGenOnDisconnect:l,autoCarousel:g,useBeta:o,imageDownloadType:a,imageResizeMode:u,baseURL:d}});/*! ***************************************************************************** + `}),a=ee(()=>2*Math.PI*g.value),u=ee(()=>t.type==="dashboard"?.75:1),d=ee(()=>`${-1*a.value*(1-u.value)/2}px`),c=ee(()=>({strokeDasharray:`${a.value*u.value}px, ${a.value}px`,strokeDashoffset:d.value})),f=ee(()=>({strokeDasharray:`${a.value*u.value*(t.percentage/100)}px, ${a.value}px`,strokeDashoffset:d.value,transition:"stroke-dasharray 0.6s ease 0s, stroke 0.6s ease, opacity ease 0.6s"})),s=ee(()=>{let _;return t.color?_=b(t.percentage):_=n[t.status]||n.default,_}),h=ee(()=>t.status==="warning"?bp:t.type==="line"?t.status==="success"?Ll:qc:t.status==="success"?__:$r),v=ee(()=>t.type==="line"?12+t.strokeWidth*.4:t.width*.111111+2),m=ee(()=>t.format(t.percentage));function y(_){const w=100/_.length;return _.map((x,k)=>Xe(x)?{color:x,percentage:(k+1)*w}:x).sort((x,k)=>x.percentage-k.percentage)}const b=_=>{var w;const{color:S}=t;if(Ue(S))return S(_);if(Xe(S))return S;{const x=y(S);for(const k of x)if(k.percentage>_)return k.color;return(w=x[x.length-1])==null?void 0:w.color}};return(_,w)=>(z(),se("div",{class:de([C(r).b(),C(r).m(_.type),C(r).is(_.status),{[C(r).m("without-text")]:!_.showText,[C(r).m("text-inside")]:_.textInside}]),role:"progressbar","aria-valuenow":_.percentage,"aria-valuemin":"0","aria-valuemax":"100"},[_.type==="line"?(z(),se("div",{key:0,class:de(C(r).b("bar"))},[ne("div",{class:de(C(r).be("bar","outer")),style:ze({height:`${_.strokeWidth}px`})},[ne("div",{class:de([C(r).be("bar","inner"),{[C(r).bem("bar","inner","indeterminate")]:_.indeterminate}]),style:ze(C(i))},[(_.showText||_.$slots.default)&&_.textInside?(z(),se("div",{key:0,class:de(C(r).be("bar","innerText"))},[xe(_.$slots,"default",{percentage:_.percentage},()=>[ne("span",null,Ae(C(m)),1)])],2)):ye("v-if",!0)],6)],6)],2)):(z(),se("div",{key:1,class:de(C(r).b("circle")),style:ze({height:`${_.width}px`,width:`${_.width}px`})},[(z(),se("svg",N9,[ne("path",{class:de(C(r).be("circle","track")),d:C(o),stroke:`var(${C(r).cssVarName("fill-color-light")}, #e5e9f2)`,"stroke-width":C(l),fill:"none",style:ze(C(c))},null,14,j9),ne("path",{class:de(C(r).be("circle","path")),d:C(o),stroke:C(s),fill:"none",opacity:_.percentage?1:0,"stroke-linecap":_.strokeLinecap,"stroke-width":C(l),style:ze(C(f))},null,14,V9)]))],6)),(_.showText||_.$slots.default)&&!_.textInside?(z(),se("div",{key:2,class:de(C(r).e("text")),style:ze({fontSize:`${C(v)}px`})},[xe(_.$slots,"default",{percentage:_.percentage},()=>[_.status?(z(),be(C(Me),{key:1},{default:he(()=>[(z(),be(Ct(C(h))))]),_:1})):(z(),se("span",U9,Ae(C(m)),1))])],6)):ye("v-if",!0)],10,z9))}});var Y9=Le(W9,[["__file","/home/runner/work/element-plus/element-plus/packages/components/progress/src/progress.vue"]]);const Ew=kt(Y9),X9=["start","center","end","space-around","space-between","space-evenly"],K9=["top","middle","bottom"],G9=Fe({tag:{type:String,default:"div"},gutter:{type:Number,default:0},justify:{type:String,values:X9,default:"start"},align:{type:String,values:K9,default:"top"}}),q9={name:"ElRow"},Z9=Ee({...q9,props:G9,setup(e){const t=e,n=Ie("row"),r=ee(()=>t.gutter);gt(T1,{gutter:r});const i=ee(()=>{const l={};return t.gutter&&(l.marginRight=l.marginLeft=`-${t.gutter/2}px`),l});return(l,g)=>(z(),be(Ct(l.tag),{class:de([C(n).b(),C(n).is(`justify-${t.justify}`,l.justify!=="start"),C(n).is(`align-${t.align}`,l.align!=="top")]),style:ze(C(i))},{default:he(()=>[xe(l.$slots,"default")]),_:3},8,["class","style"]))}});var J9=Le(Z9,[["__file","/home/runner/work/element-plus/element-plus/packages/components/row/src/row.vue"]]);const T0=kt(J9),Q9=Fe({modelValue:{type:Pe([Number,Array]),default:0},id:{type:String,default:void 0},min:{type:Number,default:0},max:{type:Number,default:100},step:{type:Number,default:1},showInput:Boolean,showInputControls:{type:Boolean,default:!0},size:Ki,inputSize:Ki,showStops:Boolean,showTooltip:{type:Boolean,default:!0},formatTooltip:{type:Pe(Function),default:void 0},disabled:Boolean,range:Boolean,vertical:Boolean,height:String,debounce:{type:Number,default:300},label:{type:String,default:void 0},rangeStartLabel:{type:String,default:void 0},rangeEndLabel:{type:String,default:void 0},formatValueText:{type:Pe(Function),default:void 0},tooltipClass:{type:String,default:void 0},placement:{type:String,values:Go,default:"top"},marks:{type:Pe(Object)},validateEvent:{type:Boolean,default:!0}}),Uf=e=>ft(e)||Ve(e)&&e.every(ft),ej={[wt]:Uf,[Ba]:Uf,[ui]:Uf},tj=(e,t,n)=>{const r=oe();return rt(async()=>{e.range?(Array.isArray(e.modelValue)?(t.firstValue=Math.max(e.min,e.modelValue[0]),t.secondValue=Math.min(e.max,e.modelValue[1])):(t.firstValue=e.min,t.secondValue=e.max),t.oldValue=[t.firstValue,t.secondValue]):(typeof e.modelValue!="number"||Number.isNaN(e.modelValue)?t.firstValue=e.min:t.firstValue=Math.min(e.max,Math.max(e.min,e.modelValue)),t.oldValue=t.firstValue),St(window,"resize",n),await He(),n()}),{sliderWrapper:r}},nj=e=>ee(()=>e.marks?Object.keys(e.marks).map(Number.parseFloat).sort((n,r)=>n-r).filter(n=>n<=e.max&&n>=e.min).map(n=>({point:n,position:(n-e.min)*100/(e.max-e.min),mark:e.marks[n]})):[]),rj=(e,t,n)=>{const{form:r,formItem:i}=ea(),l=gn(),g=oe(),o=oe(),a={firstButton:g,secondButton:o},u=ee(()=>e.disabled||(r==null?void 0:r.disabled)||!1),d=ee(()=>Math.min(t.firstValue,t.secondValue)),c=ee(()=>Math.max(t.firstValue,t.secondValue)),f=ee(()=>e.range?`${100*(c.value-d.value)/(e.max-e.min)}%`:`${100*(t.firstValue-e.min)/(e.max-e.min)}%`),s=ee(()=>e.range?`${100*(d.value-e.min)/(e.max-e.min)}%`:"0%"),h=ee(()=>e.vertical?{height:e.height}:{}),v=ee(()=>e.vertical?{height:f.value,bottom:s.value}:{width:f.value,left:s.value}),m=()=>{l.value&&(t.sliderSize=l.value[`client${e.vertical?"Height":"Width"}`])},y=N=>{const L=e.min+N*(e.max-e.min)/100;if(!e.range)return g;let F;return Math.abs(d.value-L)t.secondValue?"firstButton":"secondButton",a[F]},b=N=>{const L=y(N);return L.value.setPosition(N),L},_=N=>{t.firstValue=N,S(e.range?[d.value,c.value]:N)},w=N=>{t.secondValue=N,e.range&&S([d.value,c.value])},S=N=>{n(wt,N),n(Ba,N)},x=async()=>{await He(),n(ui,e.range?[d.value,c.value]:e.modelValue)},k=N=>{var L,F,O,V,re,J;if(u.value||t.dragging)return;m();let ue=0;if(e.vertical){const M=(O=(F=(L=N.touches)==null?void 0:L.item(0))==null?void 0:F.clientY)!=null?O:N.clientY;ue=(l.value.getBoundingClientRect().bottom-M)/t.sliderSize*100}else{const M=(J=(re=(V=N.touches)==null?void 0:V.item(0))==null?void 0:re.clientX)!=null?J:N.clientX,H=l.value.getBoundingClientRect().left;ue=(M-H)/t.sliderSize*100}if(!(ue<0||ue>100))return b(ue)};return{elFormItem:i,slider:l,firstButton:g,secondButton:o,sliderDisabled:u,minValue:d,maxValue:c,runwayStyle:h,barStyle:v,resetSize:m,setPosition:b,emitChange:x,onSliderWrapperPrevent:N=>{var L,F;(((L=a.firstButton.value)==null?void 0:L.dragging)||((F=a.secondButton.value)==null?void 0:F.dragging))&&N.preventDefault()},onSliderClick:N=>{k(N)&&x()},onSliderDown:async N=>{const L=k(N);L&&(await He(),L.value.onButtonDown(N))},setFirstValue:_,setSecondValue:w}},{left:ij,down:aj,right:oj,up:sj,home:lj,end:uj,pageUp:cj,pageDown:fj}=Ot,dj=(e,t,n)=>{const r=oe(),i=oe(!1),l=ee(()=>t.value instanceof Function),g=ee(()=>l.value&&t.value(e.modelValue)||e.modelValue),o=Ro(()=>{n.value&&(i.value=!0)},50),a=Ro(()=>{n.value&&(i.value=!1)},50);return{tooltip:r,tooltipVisible:i,formatValue:g,displayTooltip:o,hideTooltip:a}},hj=(e,t,n)=>{const{disabled:r,min:i,max:l,step:g,showTooltip:o,precision:a,sliderSize:u,formatTooltip:d,emitChange:c,resetSize:f,updateDragging:s}=Ke(O1),{tooltip:h,tooltipVisible:v,formatValue:m,displayTooltip:y,hideTooltip:b}=dj(e,d,o),_=oe(),w=ee(()=>`${(e.modelValue-i.value)/(l.value-i.value)*100}%`),S=ee(()=>e.vertical?{bottom:w.value}:{left:w.value}),x=()=>{t.hovering=!0,y()},k=()=>{t.hovering=!1,t.dragging||b()},E=P=>{r.value||(P.preventDefault(),ue(P),window.addEventListener("mousemove",M),window.addEventListener("touchmove",M),window.addEventListener("mouseup",H),window.addEventListener("touchend",H),window.addEventListener("contextmenu",H),_.value.focus())},I=P=>{r.value||(t.newPosition=Number.parseFloat(w.value)+P/(l.value-i.value)*100,R(t.newPosition),c())},D=()=>{I(-g.value)},N=()=>{I(g.value)},L=()=>{I(-g.value*4)},F=()=>{I(g.value*4)},O=()=>{r.value||(R(0),c())},V=()=>{r.value||(R(100),c())},re=P=>{let Q=!0;[ij,aj].includes(P.key)?D():[oj,sj].includes(P.key)?N():P.key===lj?O():P.key===uj?V():P.key===fj?L():P.key===cj?F():Q=!1,Q&&P.preventDefault()},J=P=>{let Q,j;return P.type.startsWith("touch")?(j=P.touches[0].clientY,Q=P.touches[0].clientX):(j=P.clientY,Q=P.clientX),{clientX:Q,clientY:j}},ue=P=>{t.dragging=!0,t.isClick=!0;const{clientX:Q,clientY:j}=J(P);e.vertical?t.startY=j:t.startX=Q,t.startPosition=Number.parseFloat(w.value),t.newPosition=t.startPosition},M=P=>{if(t.dragging){t.isClick=!1,y(),f();let Q;const{clientX:j,clientY:A}=J(P);e.vertical?(t.currentY=A,Q=(t.startY-t.currentY)/u.value*100):(t.currentX=j,Q=(t.currentX-t.startX)/u.value*100),t.newPosition=t.startPosition+Q,R(t.newPosition)}},H=()=>{t.dragging&&(setTimeout(()=>{t.dragging=!1,t.hovering||b(),t.isClick||R(t.newPosition),c()},0),window.removeEventListener("mousemove",M),window.removeEventListener("touchmove",M),window.removeEventListener("mouseup",H),window.removeEventListener("touchend",H),window.removeEventListener("contextmenu",H))},R=async P=>{if(P===null||Number.isNaN(+P))return;P<0?P=0:P>100&&(P=100);const Q=100/((l.value-i.value)/g.value);let A=Math.round(P/Q)*Q*(l.value-i.value)*.01+i.value;A=Number.parseFloat(A.toFixed(a.value)),A!==e.modelValue&&n(wt,A),!t.dragging&&e.modelValue!==t.oldValue&&(t.oldValue=e.modelValue),await He(),t.dragging&&y(),h.value.updatePopper()};return ke(()=>t.dragging,P=>{s(P)}),{disabled:r,button:_,tooltip:h,tooltipVisible:v,showTooltip:o,wrapperStyle:S,formatValue:m,handleMouseEnter:x,handleMouseLeave:k,onButtonDown:E,onKeyDown:re,setPosition:R}},pj=(e,t,n,r)=>({stops:ee(()=>{if(!e.showStops||e.min>e.max)return[];if(e.step===0)return[];const g=(e.max-e.min)/e.step,o=100*e.step/(e.max-e.min),a=Array.from({length:g-1}).map((u,d)=>(d+1)*o);return e.range?a.filter(u=>u<100*(n.value-e.min)/(e.max-e.min)||u>100*(r.value-e.min)/(e.max-e.min)):a.filter(u=>u>100*(t.firstValue-e.min)/(e.max-e.min))}),getStopStyle:g=>e.vertical?{bottom:`${g}%`}:{left:`${g}%`}}),mj=(e,t,n,r,i,l)=>{const g=u=>{i(wt,u),i(Ba,u)},o=()=>e.range?![n.value,r.value].every((u,d)=>u===t.oldValue[d]):e.modelValue!==t.oldValue,a=()=>{var u,d;if(e.min>e.max){Tn("Slider","min should not be greater than max.");return}const c=e.modelValue;e.range&&Array.isArray(c)?c[1]e.max?g([e.max,e.max]):c[0]e.max?g([c[0],e.max]):(t.firstValue=c[0],t.secondValue=c[1],o()&&(e.validateEvent&&((u=l==null?void 0:l.validate)==null||u.call(l,"change").catch(f=>void 0)),t.oldValue=c.slice())):!e.range&&typeof c=="number"&&!Number.isNaN(c)&&(ce.max?g(e.max):(t.firstValue=c,o()&&(e.validateEvent&&((d=l==null?void 0:l.validate)==null||d.call(l,"change").catch(f=>void 0)),t.oldValue=c)))};a(),ke(()=>t.dragging,u=>{u||a()}),ke(()=>e.modelValue,(u,d)=>{t.dragging||Array.isArray(u)&&Array.isArray(d)&&u.every((c,f)=>c===d[f])&&t.firstValue===u[0]&&t.secondValue===u[1]||a()},{deep:!0}),ke(()=>[e.min,e.max],()=>{a()})},gj=Fe({modelValue:{type:Number,default:0},vertical:Boolean,tooltipClass:String,placement:{type:String,values:Go,default:"top"}}),vj={[wt]:e=>ft(e)},yj=["tabindex"],bj={name:"ElSliderButton"},_j=Ee({...bj,props:gj,emits:vj,setup(e,{expose:t,emit:n}){const r=e,i=Ie("slider"),l=vt({hovering:!1,dragging:!1,isClick:!1,startX:0,currentX:0,startY:0,currentY:0,startPosition:0,newPosition:0,oldValue:r.modelValue}),{disabled:g,button:o,tooltip:a,showTooltip:u,tooltipVisible:d,wrapperStyle:c,formatValue:f,handleMouseEnter:s,handleMouseLeave:h,onButtonDown:v,onKeyDown:m,setPosition:y}=hj(r,l,n),{hovering:b,dragging:_}=rr(l);return t({onButtonDown:v,onKeyDown:m,setPosition:y,hovering:b,dragging:_}),(w,S)=>(z(),se("div",{ref_key:"button",ref:o,class:de([C(i).e("button-wrapper"),{hover:C(b),dragging:C(_)}]),style:ze(C(c)),tabindex:C(g)?-1:0,onMouseenter:S[0]||(S[0]=(...x)=>C(s)&&C(s)(...x)),onMouseleave:S[1]||(S[1]=(...x)=>C(h)&&C(h)(...x)),onMousedown:S[2]||(S[2]=(...x)=>C(v)&&C(v)(...x)),onTouchstart:S[3]||(S[3]=(...x)=>C(v)&&C(v)(...x)),onFocus:S[4]||(S[4]=(...x)=>C(s)&&C(s)(...x)),onBlur:S[5]||(S[5]=(...x)=>C(h)&&C(h)(...x)),onKeydown:S[6]||(S[6]=(...x)=>C(m)&&C(m)(...x))},[le(C(Vn),{ref_key:"tooltip",ref:a,visible:C(d),placement:w.placement,"fallback-placements":["top","bottom","right","left"],"stop-popper-mouse-event":!1,"popper-class":w.tooltipClass,disabled:!C(u),persistent:""},{content:he(()=>[ne("span",null,Ae(C(f)),1)]),default:he(()=>[ne("div",{class:de([C(i).e("button"),{hover:C(b),dragging:C(_)}])},null,2)]),_:1},8,["visible","placement","popper-class","disabled"])],46,yj))}});var E0=Le(_j,[["__file","/home/runner/work/element-plus/element-plus/packages/components/slider/src/button.vue"]]);const wj=Fe({mark:{type:Pe([String,Object]),default:void 0}});var Cj=Ee({name:"ElSliderMarker",props:wj,setup(e){const t=Ie("slider"),n=ee(()=>Xe(e.mark)?e.mark:e.mark.label),r=ee(()=>Xe(e.mark)?void 0:e.mark.style);return()=>_t("div",{class:t.e("marks-text"),style:r.value},n.value)}});const Sj=["id","role","aria-label","aria-labelledby"],xj={key:1},kj={name:"ElSlider"},Tj=Ee({...kj,props:Q9,emits:ej,setup(e,{expose:t,emit:n}){const r=e,i=Ie("slider"),{t:l}=yn(),g=vt({firstValue:0,secondValue:0,oldValue:0,dragging:!1,sliderSize:1}),{elFormItem:o,slider:a,firstButton:u,secondButton:d,sliderDisabled:c,minValue:f,maxValue:s,runwayStyle:h,barStyle:v,resetSize:m,emitChange:y,onSliderWrapperPrevent:b,onSliderClick:_,onSliderDown:w,setFirstValue:S,setSecondValue:x}=rj(r,g,n),{stops:k,getStopStyle:E}=pj(r,g,f,s),{inputId:I,isLabeledByFormItem:D}=$l(r,{formItemContext:o}),N=sr(),L=ee(()=>r.inputSize||N.value),F=ee(()=>r.label||l("el.slider.defaultLabel",{min:r.min,max:r.max})),O=ee(()=>r.range?r.rangeStartLabel||l("el.slider.defaultRangeStartLabel"):F.value),V=ee(()=>r.formatValueText?r.formatValueText(P.value):`${P.value}`),re=ee(()=>r.rangeEndLabel||l("el.slider.defaultRangeEndLabel")),J=ee(()=>r.formatValueText?r.formatValueText(Q.value):`${Q.value}`),ue=ee(()=>[i.b(),i.m(N.value),i.is("vertical",r.vertical),{[i.m("with-input")]:r.showInput}]),M=nj(r);mj(r,g,f,s,n,o);const H=ee(()=>{const q=[r.min,r.max,r.step].map(G=>{const te=`${G}`.split(".")[1];return te?te.length:0});return Math.max.apply(null,q)}),{sliderWrapper:R}=tj(r,g,m),{firstValue:P,secondValue:Q,sliderSize:j}=rr(g),A=q=>{g.dragging=q};return gt(O1,{...rr(r),sliderSize:j,disabled:c,precision:H,emitChange:y,resetSize:m,updateDragging:A}),t({onSliderClick:_}),(q,G)=>{var te,fe;return z(),se("div",{id:q.range?C(I):void 0,ref_key:"sliderWrapper",ref:R,class:de(C(ue)),role:q.range?"group":void 0,"aria-label":q.range&&!C(D)?C(F):void 0,"aria-labelledby":q.range&&C(D)?(te=C(o))==null?void 0:te.labelId:void 0,onTouchstart:G[2]||(G[2]=(...pe)=>C(b)&&C(b)(...pe)),onTouchmove:G[3]||(G[3]=(...pe)=>C(b)&&C(b)(...pe))},[ne("div",{ref_key:"slider",ref:a,class:de([C(i).e("runway"),{"show-input":q.showInput&&!q.range},C(i).is("disabled",C(c))]),style:ze(C(h)),onMousedown:G[0]||(G[0]=(...pe)=>C(w)&&C(w)(...pe)),onTouchstart:G[1]||(G[1]=(...pe)=>C(w)&&C(w)(...pe))},[ne("div",{class:de(C(i).e("bar")),style:ze(C(v))},null,6),le(E0,{id:q.range?void 0:C(I),ref_key:"firstButton",ref:u,"model-value":C(P),vertical:q.vertical,"tooltip-class":q.tooltipClass,placement:q.placement,role:"slider","aria-label":q.range||!C(D)?C(O):void 0,"aria-labelledby":!q.range&&C(D)?(fe=C(o))==null?void 0:fe.labelId:void 0,"aria-valuemin":q.min,"aria-valuemax":q.range?C(Q):q.max,"aria-valuenow":C(P),"aria-valuetext":C(V),"aria-orientation":q.vertical?"vertical":"horizontal","aria-disabled":C(c),"onUpdate:modelValue":C(S)},null,8,["id","model-value","vertical","tooltip-class","placement","aria-label","aria-labelledby","aria-valuemin","aria-valuemax","aria-valuenow","aria-valuetext","aria-orientation","aria-disabled","onUpdate:modelValue"]),q.range?(z(),be(E0,{key:0,ref_key:"secondButton",ref:d,"model-value":C(Q),vertical:q.vertical,"tooltip-class":q.tooltipClass,placement:q.placement,role:"slider","aria-label":C(re),"aria-valuemin":C(P),"aria-valuemax":q.max,"aria-valuenow":C(Q),"aria-valuetext":C(J),"aria-orientation":q.vertical?"vertical":"horizontal","aria-disabled":C(c),"onUpdate:modelValue":C(x)},null,8,["model-value","vertical","tooltip-class","placement","aria-label","aria-valuemin","aria-valuemax","aria-valuenow","aria-valuetext","aria-orientation","aria-disabled","onUpdate:modelValue"])):ye("v-if",!0),q.showStops?(z(),se("div",xj,[(z(!0),se(Ne,null,Nt(C(k),(pe,ve)=>(z(),se("div",{key:ve,class:de(C(i).e("stop")),style:ze(C(E)(pe))},null,6))),128))])):ye("v-if",!0),C(M).length>0?(z(),se(Ne,{key:2},[ne("div",null,[(z(!0),se(Ne,null,Nt(C(M),(pe,ve)=>(z(),se("div",{key:ve,style:ze(C(E)(pe.position)),class:de([C(i).e("stop"),C(i).e("marks-stop")])},null,6))),128))]),ne("div",{class:de(C(i).e("marks"))},[(z(!0),se(Ne,null,Nt(C(M),(pe,ve)=>(z(),be(C(Cj),{key:ve,mark:pe.mark,style:ze(C(E)(pe.position))},null,8,["mark","style"]))),128))],2)],64)):ye("v-if",!0)],38),q.showInput&&!q.range?(z(),be(C(ih),{key:0,ref:"input","model-value":C(P),class:de(C(i).e("input")),step:q.step,disabled:C(c),controls:q.showInputControls,min:q.min,max:q.max,debounce:q.debounce,size:C(L),"onUpdate:modelValue":C(S),onChange:C(y)},null,8,["model-value","class","step","disabled","controls","min","max","debounce","size","onUpdate:modelValue","onChange"])):ye("v-if",!0)],42,Sj)}}});var Ej=Le(Tj,[["__file","/home/runner/work/element-plus/element-plus/packages/components/slider/src/slider.vue"]]);const O0=kt(Ej),Oj=Fe({modelValue:{type:[Boolean,String,Number],default:!1},value:{type:[Boolean,String,Number],default:!1},disabled:{type:Boolean,default:!1},width:{type:[String,Number],default:""},inlinePrompt:{type:Boolean,default:!1},activeIcon:{type:Wt},inactiveIcon:{type:Wt},activeText:{type:String,default:""},inactiveText:{type:String,default:""},activeColor:{type:String,default:""},inactiveColor:{type:String,default:""},borderColor:{type:String,default:""},activeValue:{type:[Boolean,String,Number],default:!0},inactiveValue:{type:[Boolean,String,Number],default:!1},name:{type:String,default:""},validateEvent:{type:Boolean,default:!0},id:String,loading:{type:Boolean,default:!1},beforeChange:{type:Pe(Function)},size:{type:String,validator:nf},tabindex:{type:[String,Number]}}),Aj={[wt]:e=>Yn(e)||Xe(e)||ft(e),[ui]:e=>Yn(e)||Xe(e)||ft(e),[Ba]:e=>Yn(e)||Xe(e)||ft(e)},Pj=["onClick"],Ij=["id","aria-checked","aria-disabled","name","true-value","false-value","disabled","tabindex","onKeydown"],Mj=["aria-hidden"],Lj=["aria-hidden"],Rj=["aria-hidden"],Dj=["aria-hidden"],$j={name:"ElSwitch"},Bj=Ee({...$j,props:Oj,emits:Aj,setup(e,{expose:t,emit:n}){const r=e,i="ElSwitch",l=xt(),{formItem:g}=ea(),o=sr(),a=Ie("switch");al({from:'"value"',replacement:'"model-value" or "v-model"',scope:i,version:"2.3.0",ref:"https://element-plus.org/en-US/component/switch.html#attributes",type:"Attribute"},ee(()=>{var x;return!!((x=l.vnode.props)!=null&&x.value)}));const{inputId:u}=$l(r,{formItemContext:g}),d=Ja(ee(()=>r.loading)),c=oe(r.modelValue!==!1),f=oe(),s=oe(),h=ee(()=>[a.b(),a.m(o.value),a.is("disabled",d.value),a.is("checked",y.value)]),v=ee(()=>({width:Fr(r.width)}));ke(()=>r.modelValue,()=>{c.value=!0}),ke(()=>r.value,()=>{c.value=!1});const m=ee(()=>c.value?r.modelValue:r.value),y=ee(()=>m.value===r.activeValue);[r.activeValue,r.inactiveValue].includes(m.value)||(n(wt,r.inactiveValue),n(ui,r.inactiveValue),n(Ba,r.inactiveValue)),ke(y,x=>{var k;f.value.checked=x,r.validateEvent&&((k=g==null?void 0:g.validate)==null||k.call(g,"change").catch(E=>void 0))});const b=()=>{const x=y.value?r.inactiveValue:r.activeValue;n(wt,x),n(ui,x),n(Ba,x),He(()=>{f.value.checked=y.value})},_=()=>{if(d.value)return;const{beforeChange:x}=r;if(!x){b();return}const k=x();[nc(k),Yn(k)].includes(!0)||Tn(i,"beforeChange must return type `Promise` or `boolean`"),nc(k)?k.then(I=>{I&&b()}).catch(I=>{}):k&&b()},w=ee(()=>a.cssVarBlock({...r.activeColor?{"on-color":r.activeColor}:null,...r.inactiveColor?{"off-color":r.inactiveColor}:null,...r.borderColor?{"border-color":r.borderColor}:null})),S=()=>{var x,k;(k=(x=f.value)==null?void 0:x.focus)==null||k.call(x)};return rt(()=>{f.value.checked=y.value}),t({focus:S,checked:y}),(x,k)=>(z(),se("div",{class:de(C(h)),style:ze(C(w)),onClick:tt(_,["prevent"])},[ne("input",{id:C(u),ref_key:"input",ref:f,class:de(C(a).e("input")),type:"checkbox",role:"switch","aria-checked":C(y),"aria-disabled":C(d),name:x.name,"true-value":x.activeValue,"false-value":x.inactiveValue,disabled:C(d),tabindex:x.tabindex,onChange:b,onKeydown:$t(_,["enter"])},null,42,Ij),!x.inlinePrompt&&(x.inactiveIcon||x.inactiveText)?(z(),se("span",{key:0,class:de([C(a).e("label"),C(a).em("label","left"),C(a).is("active",!C(y))])},[x.inactiveIcon?(z(),be(C(Me),{key:0},{default:he(()=>[(z(),be(Ct(x.inactiveIcon)))]),_:1})):ye("v-if",!0),!x.inactiveIcon&&x.inactiveText?(z(),se("span",{key:1,"aria-hidden":C(y)},Ae(x.inactiveText),9,Mj)):ye("v-if",!0)],2)):ye("v-if",!0),ne("span",{ref_key:"core",ref:s,class:de(C(a).e("core")),style:ze(C(v))},[x.inlinePrompt?(z(),se("div",{key:0,class:de(C(a).e("inner"))},[x.activeIcon||x.inactiveIcon?(z(),se(Ne,{key:0},[x.activeIcon?(z(),be(C(Me),{key:0,class:de([C(a).is("icon"),C(y)?C(a).is("show"):C(a).is("hide")])},{default:he(()=>[(z(),be(Ct(x.activeIcon)))]),_:1},8,["class"])):ye("v-if",!0),x.inactiveIcon?(z(),be(C(Me),{key:1,class:de([C(a).is("icon"),C(y)?C(a).is("hide"):C(a).is("show")])},{default:he(()=>[(z(),be(Ct(x.inactiveIcon)))]),_:1},8,["class"])):ye("v-if",!0)],64)):x.activeText||x.inactiveIcon?(z(),se(Ne,{key:1},[x.activeText?(z(),se("span",{key:0,class:de([C(a).is("text"),C(y)?C(a).is("show"):C(a).is("hide")]),"aria-hidden":!C(y)},Ae(x.activeText.substring(0,3)),11,Lj)):ye("v-if",!0),x.inactiveText?(z(),se("span",{key:1,class:de([C(a).is("text"),C(y)?C(a).is("hide"):C(a).is("show")]),"aria-hidden":C(y)},Ae(x.inactiveText.substring(0,3)),11,Rj)):ye("v-if",!0)],64)):ye("v-if",!0)],2)):ye("v-if",!0),ne("div",{class:de(C(a).e("action"))},[x.loading?(z(),be(C(Me),{key:0,class:de(C(a).is("loading"))},{default:he(()=>[le(C(gp))]),_:1},8,["class"])):ye("v-if",!0)],2)],6),!x.inlinePrompt&&(x.activeIcon||x.activeText)?(z(),se("span",{key:1,class:de([C(a).e("label"),C(a).em("label","right"),C(a).is("active",C(y))])},[x.activeIcon?(z(),be(C(Me),{key:0},{default:he(()=>[(z(),be(Ct(x.activeIcon)))]),_:1})):ye("v-if",!0),!x.activeIcon&&x.activeText?(z(),se("span",{key:1,"aria-hidden":!C(y)},Ae(x.activeText),9,Dj)):ye("v-if",!0)],2)):ye("v-if",!0)],14,Pj))}});var Fj=Le(Bj,[["__file","/home/runner/work/element-plus/element-plus/packages/components/switch/src/switch.vue"]]);const A0=kt(Fj),zj=Fe({tabs:{type:Pe(Array),default:()=>sn([])}}),Nj={name:"ElTabBar"},jj=Ee({...Nj,props:zj,setup(e,{expose:t}){const n=e,r="ElTabBar",i=xt(),l=Ke(rf);l||Tn(r,"");const g=Ie("tabs"),o=oe(),a=oe(),u=()=>{let c=0,f=0;const s=["top","bottom"].includes(l.props.tabPosition)?"width":"height",h=s==="width"?"x":"y";return n.tabs.every(v=>{var m,y,b,_;const w=(y=(m=i.parent)==null?void 0:m.refs)==null?void 0:y[`tab-${v.uid}`];if(!w)return!1;if(!v.active)return!0;f=w[`client${ya(s)}`];const S=h==="x"?"left":"top";c=w.getBoundingClientRect()[S]-((_=(b=w.parentElement)==null?void 0:b.getBoundingClientRect()[S])!=null?_:0);const x=window.getComputedStyle(w);return s==="width"&&(n.tabs.length>1&&(f-=Number.parseFloat(x.paddingLeft)+Number.parseFloat(x.paddingRight)),c+=Number.parseFloat(x.paddingLeft)),!1}),{[s]:`${f}px`,transform:`translate${ya(h)}(${c}px)`}},d=()=>a.value=u();return ke(()=>n.tabs,async()=>{await He(),d()},{immediate:!0}),Nr(o,()=>d()),t({ref:o,update:d}),(c,f)=>(z(),se("div",{ref_key:"barRef",ref:o,class:de([C(g).e("active-bar"),C(g).is(C(l).props.tabPosition)]),style:ze(a.value)},null,6))}});var Vj=Le(jj,[["__file","/home/runner/work/element-plus/element-plus/packages/components/tabs/src/tab-bar.vue"]]);const Uj=Fe({panes:{type:Pe(Array),default:()=>sn([])},currentName:{type:[String,Number],default:""},editable:Boolean,type:{type:String,values:["card","border-card",""],default:""},stretch:Boolean}),Hj={tabClick:(e,t,n)=>n instanceof Event,tabRemove:(e,t)=>t instanceof Event},P0="ElTabNav",Wj=Ee({name:P0,props:Uj,emits:Hj,setup(e,{expose:t,emit:n}){const r=xt(),i=Ke(rf);i||Tn(P0,"");const l=Ie("tabs"),g=dM(),o=TM(),a=oe(),u=oe(),d=oe(),c=oe(!1),f=oe(0),s=oe(!1),h=oe(!0),v=ee(()=>["top","bottom"].includes(i.props.tabPosition)?"width":"height"),m=ee(()=>({transform:`translate${v.value==="width"?"X":"Y"}(-${f.value}px)`})),y=()=>{if(!a.value)return;const E=a.value[`offset${ya(v.value)}`],I=f.value;if(!I)return;const D=I>E?I-E:0;f.value=D},b=()=>{if(!a.value||!u.value)return;const E=u.value[`offset${ya(v.value)}`],I=a.value[`offset${ya(v.value)}`],D=f.value;if(E-D<=I)return;const N=E-D>I*2?D+I:E-I;f.value=N},_=async()=>{const E=u.value;if(!c.value||!d.value||!a.value||!E)return;await He();const I=d.value.querySelector(".is-active");if(!I)return;const D=a.value,N=["top","bottom"].includes(i.props.tabPosition),L=I.getBoundingClientRect(),F=D.getBoundingClientRect(),O=N?E.offsetWidth-F.width:E.offsetHeight-F.height,V=f.value;let re=V;N?(L.leftF.right&&(re=V+L.right-F.right)):(L.topF.bottom&&(re=V+(L.bottom-F.bottom))),re=Math.max(re,0),f.value=Math.min(re,O)},w=()=>{if(!u.value||!a.value)return;const E=u.value[`offset${ya(v.value)}`],I=a.value[`offset${ya(v.value)}`],D=f.value;if(I0&&(f.value=0)},S=E=>{const I=E.code,{up:D,down:N,left:L,right:F}=Ot;if(![D,N,L,F].includes(I))return;const O=Array.from(E.currentTarget.querySelectorAll("[role=tab]:not(.is-disabled)")),V=O.indexOf(E.target);let re;I===L||I===D?V===0?re=O.length-1:re=V-1:V{h.value&&(s.value=!0)},k=()=>s.value=!1;return ke(g,E=>{E==="hidden"?h.value=!1:E==="visible"&&setTimeout(()=>h.value=!0,50)}),ke(o,E=>{E?setTimeout(()=>h.value=!0,50):h.value=!1}),Nr(d,w),rt(()=>setTimeout(()=>_(),0)),Ga(()=>w()),t({scrollToActiveTab:_,removeFocus:k}),ke(()=>e.panes,()=>r.update(),{flush:"post"}),()=>{const E=c.value?[le("span",{class:[l.e("nav-prev"),l.is("disabled",!c.value.prev)],onClick:y},[le(Me,null,{default:()=>[le(Gc,null,null)]})]),le("span",{class:[l.e("nav-next"),l.is("disabled",!c.value.next)],onClick:b},[le(Me,null,{default:()=>[le(Ml,null,null)]})])]:null,I=e.panes.map((D,N)=>{var L,F,O,V;const re=D.uid,J=D.props.disabled,ue=(F=(L=D.props.name)!=null?L:D.index)!=null?F:`${N}`,M=!J&&(D.isClosable||e.editable);D.index=`${N}`;const H=M?le(Me,{class:"is-icon-close",onClick:Q=>n("tabRemove",D,Q)},{default:()=>[le($r,null,null)]}):null,R=((V=(O=D.slots).label)==null?void 0:V.call(O))||D.props.label,P=!J&&D.active?0:-1;return le("div",{ref:`tab-${re}`,class:[l.e("item"),l.is(i.props.tabPosition),l.is("active",D.active),l.is("disabled",J),l.is("closable",M),l.is("focus",s.value)],id:`tab-${ue}`,key:`tab-${re}`,"aria-controls":`pane-${ue}`,role:"tab","aria-selected":D.active,tabindex:P,onFocus:()=>x(),onBlur:()=>k(),onClick:Q=>{k(),n("tabClick",D,ue,Q)},onKeydown:Q=>{M&&(Q.code===Ot.delete||Q.code===Ot.backspace)&&n("tabRemove",D,Q)}},[R,H])});return le("div",{ref:d,class:[l.e("nav-wrap"),l.is("scrollable",!!c.value),l.is(i.props.tabPosition)]},[E,le("div",{class:l.e("nav-scroll"),ref:a},[le("div",{class:[l.e("nav"),l.is(i.props.tabPosition),l.is("stretch",e.stretch&&["top","bottom"].includes(i.props.tabPosition))],ref:u,style:m.value,role:"tablist",onKeydown:S},[e.type?null:le(Vj,{tabs:[...e.panes]},null),I])])])}}}),Yj=Fe({type:{type:String,values:["card","border-card",""],default:""},activeName:{type:[String,Number]},closable:Boolean,addable:Boolean,modelValue:{type:[String,Number]},editable:Boolean,tabPosition:{type:String,values:["top","right","bottom","left"],default:"top"},beforeLeave:{type:Pe(Function),default:()=>!0},stretch:Boolean}),Hf=e=>Xe(e)||ft(e),Xj={[wt]:e=>Hf(e),tabClick:(e,t)=>t instanceof Event,tabChange:e=>Hf(e),edit:(e,t)=>["remove","add"].includes(t),tabRemove:e=>Hf(e),tabAdd:()=>!0};var Kj=Ee({name:"ElTabs",props:Yj,emits:Xj,setup(e,{emit:t,slots:n,expose:r}){var i,l;const g=Ie("tabs"),o=oe(),a=vt({}),u=oe((l=(i=e.modelValue)!=null?i:e.activeName)!=null?l:"0"),d=v=>{u.value=v,t(wt,v),t("tabChange",v)},c=async v=>{var m,y,b;if(!(u.value===v||pr(v)))try{await((m=e.beforeLeave)==null?void 0:m.call(e,v,u.value))!==!1&&(d(v),(b=(y=o.value)==null?void 0:y.removeFocus)==null||b.call(y))}catch{}},f=(v,m,y)=>{v.props.disabled||(c(m),t("tabClick",v,y))},s=(v,m)=>{v.props.disabled||pr(v.props.name)||(m.stopPropagation(),t("edit",v.props.name,"remove"),t("tabRemove",v.props.name))},h=()=>{t("edit",void 0,"add"),t("tabAdd")};return al({from:'"activeName"',replacement:'"model-value" or "v-model"',scope:"ElTabs",version:"2.3.0",ref:"https://element-plus.org/en-US/component/tabs.html#attributes",type:"Attribute"},ee(()=>!!e.activeName)),ke(()=>e.activeName,v=>c(v)),ke(()=>e.modelValue,v=>c(v)),ke(u,async()=>{var v;await He(),(v=o.value)==null||v.scrollToActiveTab()}),gt(rf,{props:e,currentName:u,registerPane:y=>a[y.uid]=y,unregisterPane:y=>delete a[y]}),r({currentName:u}),()=>{const v=e.editable||e.addable?le("span",{class:g.e("new-tab"),tabindex:"0",onClick:h,onKeydown:b=>{b.code===Ot.enter&&h()}},[le(Me,{class:g.is("icon-plus")},{default:()=>[le(vp,null,null)]})]):null,m=le("div",{class:[g.e("header"),g.is(e.tabPosition)]},[v,le(Wj,{ref:o,currentName:u.value,editable:e.editable,type:e.type,panes:Object.values(a),stretch:e.stretch,onTabClick:f,onTabRemove:s},null)]),y=le("div",{class:g.e("content")},[xe(n,"default")]);return le("div",{class:[g.b(),g.m(e.tabPosition),{[g.m("card")]:e.type==="card",[g.m("border-card")]:e.type==="border-card"}]},[...e.tabPosition!=="bottom"?[m,y]:[y,m]])}}});const Gj=Fe({label:{type:String,default:""},name:{type:[String,Number]},closable:Boolean,disabled:Boolean,lazy:Boolean}),qj=["id","aria-hidden","aria-labelledby"],Zj={name:"ElTabPane"},Jj=Ee({...Zj,props:Gj,setup(e){const t=e,n="ElTabPane",r=xt(),i=Ho(),l=Ke(rf);l||Tn(n,"usage: ");const g=Ie("tab-pane"),o=oe(),a=ee(()=>t.closable||l.props.closable),u=bv(()=>{var h;return l.currentName.value===((h=t.name)!=null?h:o.value)}),d=oe(u.value),c=ee(()=>{var h;return(h=t.name)!=null?h:o.value}),f=bv(()=>!t.lazy||d.value||u.value);ke(u,h=>{h&&(d.value=!0)});const s=vt({uid:r.uid,slots:i,props:t,paneName:c,active:u,index:o,isClosable:a});return rt(()=>{l.registerPane(s)}),gi(()=>{l.unregisterPane(s.uid)}),(h,v)=>C(f)?Et((z(),se("div",{key:0,id:`pane-${C(c)}`,class:de(C(g).b()),role:"tabpanel","aria-hidden":!C(u),"aria-labelledby":`tab-${C(c)}`},[xe(h.$slots,"default")],10,qj)),[[Ht,C(u)]]):ye("v-if",!0)}});var Ow=Le(Jj,[["__file","/home/runner/work/element-plus/element-plus/packages/components/tabs/src/tab-pane.vue"]]);const Qj=kt(Kj,{TabPane:Ow}),Wf=wr(Ow),eV="ElUpload";class tV extends Error{constructor(t,n,r,i){super(t),this.name="UploadAjaxError",this.status=n,this.method=r,this.url=i}}function I0(e,t,n){let r;return n.response?r=`${n.response.error||n.response}`:n.responseText?r=`${n.responseText}`:r=`fail to ${t.method} ${e} ${n.status}`,new tV(r,n.status,t.method,e)}function nV(e){const t=e.responseText||e.response;if(!t)return t;try{return JSON.parse(t)}catch{return t}}const rV=e=>{typeof XMLHttpRequest>"u"&&Tn(eV,"XMLHttpRequest is undefined");const t=new XMLHttpRequest,n=e.action;t.upload&&t.upload.addEventListener("progress",l=>{const g=l;g.percent=l.total>0?l.loaded/l.total*100:0,e.onProgress(g)});const r=new FormData;if(e.data)for(const[l,g]of Object.entries(e.data))Array.isArray(g)?r.append(l,...g):r.append(l,g);r.append(e.filename,e.file,e.file.name),t.addEventListener("error",()=>{e.onError(I0(n,e,t))}),t.addEventListener("load",()=>{if(t.status<200||t.status>=300)return e.onError(I0(n,e,t));e.onSuccess(nV(t))}),t.open(e.method,n,!0),e.withCredentials&&"withCredentials"in t&&(t.withCredentials=!0);const i=e.headers||{};if(i instanceof Headers)i.forEach((l,g)=>t.setRequestHeader(g,l));else for(const[l,g]of Object.entries(i))Mr(g)||t.setRequestHeader(l,String(g));return t.send(r),t},Aw=["text","picture","picture-card"];let iV=1;const Pw=()=>Date.now()+iV++,Iw=Fe({action:{type:String,default:"#"},headers:{type:Pe(Object)},method:{type:String,default:"post"},data:{type:Object,default:()=>sn({})},multiple:{type:Boolean,default:!1},name:{type:String,default:"file"},drag:{type:Boolean,default:!1},withCredentials:Boolean,showFileList:{type:Boolean,default:!0},accept:{type:String,default:""},type:{type:String,default:"select"},fileList:{type:Pe(Array),default:()=>sn([])},autoUpload:{type:Boolean,default:!0},listType:{type:String,values:Aw,default:"text"},httpRequest:{type:Pe(Function),default:rV},disabled:Boolean,limit:Number}),aV=Fe({...Iw,beforeUpload:{type:Pe(Function),default:ct},beforeRemove:{type:Pe(Function)},onRemove:{type:Pe(Function),default:ct},onChange:{type:Pe(Function),default:ct},onPreview:{type:Pe(Function),default:ct},onSuccess:{type:Pe(Function),default:ct},onProgress:{type:Pe(Function),default:ct},onError:{type:Pe(Function),default:ct},onExceed:{type:Pe(Function),default:ct}}),oV=Fe({files:{type:Pe(Array),default:()=>sn([])},disabled:{type:Boolean,default:!1},handlePreview:{type:Pe(Function),default:ct},listType:{type:String,values:Aw,default:"text"}}),sV={remove:e=>!!e},lV=["onKeydown"],uV=["src"],cV=["onClick"],fV=["onClick"],dV=["onClick"],hV={name:"ElUploadList"},pV=Ee({...hV,props:oV,emits:sV,setup(e,{emit:t}){const{t:n}=yn(),r=Ie("upload"),i=Ie("icon"),l=Ie("list"),g=oe(!1),o=a=>{t("remove",a)};return(a,u)=>(z(),be(qx,{tag:"ul",class:de([C(r).b("list"),C(r).bm("list",a.listType),C(r).is("disabled",a.disabled)]),name:C(l).b()},{default:he(()=>[(z(!0),se(Ne,null,Nt(a.files,d=>(z(),se("li",{key:d.uid||d.name,class:de([C(r).be("list","item"),C(r).is(d.status),{focusing:g.value}]),tabindex:"0",onKeydown:$t(c=>!a.disabled&&o(d),["delete"]),onFocus:u[0]||(u[0]=c=>g.value=!0),onBlur:u[1]||(u[1]=c=>g.value=!1),onClick:u[2]||(u[2]=c=>g.value=!1)},[xe(a.$slots,"default",{file:d},()=>[a.listType==="picture"||d.status!=="uploading"&&a.listType==="picture-card"?(z(),se("img",{key:0,class:de(C(r).be("list","item-thumbnail")),src:d.url,alt:""},null,10,uV)):ye("v-if",!0),d.status==="uploading"||a.listType!=="picture-card"?(z(),se("div",{key:1,class:de(C(r).be("list","item-info"))},[ne("a",{class:de(C(r).be("list","item-name")),onClick:tt(c=>a.handlePreview(d),["prevent"])},[le(C(Me),{class:de(C(i).m("document"))},{default:he(()=>[le(C(S_))]),_:1},8,["class"]),ne("span",{class:de(C(r).be("list","item-file-name"))},Ae(d.name),3)],10,cV),d.status==="uploading"?(z(),be(C(Ew),{key:0,type:a.listType==="picture-card"?"circle":"line","stroke-width":a.listType==="picture-card"?6:2,percentage:Number(d.percentage),style:ze(a.listType==="picture-card"?"":"margin-top: 0.5rem")},null,8,["type","stroke-width","percentage","style"])):ye("v-if",!0)],2)):ye("v-if",!0),ne("label",{class:de(C(r).be("list","item-status-label"))},[a.listType==="text"?(z(),be(C(Me),{key:0,class:de([C(i).m("upload-success"),C(i).m("circle-check")])},{default:he(()=>[le(C(Ll))]),_:1},8,["class"])):["picture-card","picture"].includes(a.listType)?(z(),be(C(Me),{key:1,class:de([C(i).m("upload-success"),C(i).m("check")])},{default:he(()=>[le(C(__))]),_:1},8,["class"])):ye("v-if",!0)],2),a.disabled?ye("v-if",!0):(z(),be(C(Me),{key:2,class:de(C(i).m("close")),onClick:c=>o(d)},{default:he(()=>[le(C($r))]),_:2},1032,["class","onClick"])),ye(" Due to close btn only appears when li gets focused disappears after li gets blurred, thus keyboard navigation can never reach close btn"),ye(" This is a bug which needs to be fixed "),ye(" TODO: Fix the incorrect navigation interaction "),a.disabled?ye("v-if",!0):(z(),se("i",{key:3,class:de(C(i).m("close-tip"))},Ae(C(n)("el.upload.deleteTip")),3)),a.listType==="picture-card"?(z(),se("span",{key:4,class:de(C(r).be("list","item-actions"))},[ne("span",{class:de(C(r).be("list","item-preview")),onClick:c=>a.handlePreview(d)},[le(C(Me),{class:de(C(i).m("zoom-in"))},{default:he(()=>[le(C(A_))]),_:1},8,["class"])],10,fV),a.disabled?ye("v-if",!0):(z(),se("span",{key:0,class:de(C(r).be("list","item-delete")),onClick:c=>o(d)},[le(C(Me),{class:de(C(i).m("delete"))},{default:he(()=>[le(C(Rl))]),_:1},8,["class"])],10,dV))],2)):ye("v-if",!0)])],42,lV))),128)),xe(a.$slots,"append")]),_:3},8,["class","name"]))}});var M0=Le(pV,[["__file","/home/runner/work/element-plus/element-plus/packages/components/upload/src/upload-list.vue"]]);const mV=Fe({disabled:{type:Boolean,default:!1}}),gV={file:e=>Ve(e)},vV=["onDrop","onDragover"],yV={name:"ElUploadDrag"},bV=Ee({...yV,props:mV,emits:gV,setup(e,{emit:t}){const n=e,r="ElUploadDrag",i=Ke(A1);i||Tn(r,"usage: ");const l=Ie("upload"),g=oe(!1),o=u=>{if(n.disabled)return;g.value=!1;const d=Array.from(u.dataTransfer.files),c=i.accept.value;if(!c){t("file",d);return}const f=d.filter(s=>{const{type:h,name:v}=s,m=v.includes(".")?`.${v.split(".").pop()}`:"",y=h.replace(/\/.*$/,"");return c.split(",").map(b=>b.trim()).filter(b=>b).some(b=>b.startsWith(".")?m===b:/\/\*$/.test(b)?y===b.replace(/\/\*$/,""):/^[^/]+\/[^/]+$/.test(b)?h===b:!1)});t("file",f)},a=()=>{n.disabled||(g.value=!0)};return(u,d)=>(z(),se("div",{class:de([C(l).b("dragger"),C(l).is("dragover",g.value)]),onDrop:tt(o,["prevent"]),onDragover:tt(a,["prevent"]),onDragleave:d[0]||(d[0]=tt(c=>g.value=!1,["prevent"]))},[xe(u.$slots,"default")],42,vV))}});var _V=Le(bV,[["__file","/home/runner/work/element-plus/element-plus/packages/components/upload/src/upload-dragger.vue"]]);const wV=Fe({...Iw,beforeUpload:{type:Pe(Function),default:ct},onRemove:{type:Pe(Function),default:ct},onStart:{type:Pe(Function),default:ct},onSuccess:{type:Pe(Function),default:ct},onProgress:{type:Pe(Function),default:ct},onError:{type:Pe(Function),default:ct},onExceed:{type:Pe(Function),default:ct}}),CV=["onKeydown"],SV=["name","multiple","accept"],xV={name:"ElUploadContent",inheritAttrs:!1},kV=Ee({...xV,props:wV,setup(e,{expose:t}){const n=e,r=Ie("upload"),i=gn({}),l=gn(),g=s=>{if(s.length===0)return;const{autoUpload:h,limit:v,fileList:m,multiple:y,onStart:b,onExceed:_}=n;if(v&&m.length+s.length>v){_(s,m);return}y||(s=s.slice(0,1));for(const w of s){const S=w;S.uid=Pw(),b(S),h&&o(S)}},o=async s=>{if(l.value.value="",!n.beforeUpload)return a(s);let h;try{h=await n.beforeUpload(s)}catch{h=!1}if(h===!1){n.onRemove(s);return}let v=s;h instanceof Blob&&(h instanceof File?v=h:v=new File([h],s.name,{type:s.type})),a(Object.assign(v,{uid:s.uid}))},a=s=>{const{headers:h,data:v,method:m,withCredentials:y,name:b,action:_,onProgress:w,onSuccess:S,onError:x,httpRequest:k}=n,{uid:E}=s,I={headers:h||{},withCredentials:y,file:s,data:v,method:m,filename:b,action:_,onProgress:N=>{w(N,s)},onSuccess:N=>{S(N,s),delete i.value[E]},onError:N=>{x(N,s),delete i.value[E]}},D=k(I);i.value[E]=D,D instanceof Promise&&D.then(I.onSuccess,I.onError)},u=s=>{const h=s.target.files;!h||g(Array.from(h))},d=()=>{n.disabled||(l.value.value="",l.value.click())},c=()=>{d()};return t({abort:s=>{AM(i.value).filter(s?([v])=>String(s.uid)===v:()=>!0).forEach(([v,m])=>{m instanceof XMLHttpRequest&&m.abort(),delete i.value[v]})},upload:o}),(s,h)=>(z(),se("div",{class:de([C(r).b(),C(r).m(s.listType),C(r).is("drag",s.drag)]),tabindex:"0",onClick:d,onKeydown:$t(tt(c,["self"]),["enter","space"])},[s.drag?(z(),be(_V,{key:0,disabled:s.disabled,onFile:g},{default:he(()=>[xe(s.$slots,"default")]),_:3},8,["disabled"])):xe(s.$slots,"default",{key:1}),ne("input",{ref_key:"inputRef",ref:l,class:de(C(r).e("input")),name:s.name,multiple:s.multiple,accept:s.accept,type:"file",onChange:u,onClick:h[0]||(h[0]=tt(()=>{},["stop"]))},null,42,SV)],42,CV))}});var L0=Le(kV,[["__file","/home/runner/work/element-plus/element-plus/packages/components/upload/src/upload-content.vue"]]);const R0="ElUpload",TV=e=>{var t;(t=e.url)!=null&&t.startsWith("blob:")&&URL.revokeObjectURL(e.url)},EV=(e,t)=>{const n=kM(e,"fileList",void 0,{passive:!0}),r=f=>n.value.find(s=>s.uid===f.uid);function i(f){var s;(s=t.value)==null||s.abort(f)}function l(f=["ready","uploading","success","fail"]){n.value=n.value.filter(s=>!f.includes(s.status))}const g=(f,s)=>{const h=r(s);!h||(console.error(f),h.status="fail",n.value.splice(n.value.indexOf(h),1),e.onError(f,h,n.value),e.onChange(h,n.value))},o=(f,s)=>{const h=r(s);!h||(e.onProgress(f,h,n.value),h.status="uploading",h.percentage=Math.round(f.percent))},a=(f,s)=>{const h=r(s);!h||(h.status="success",h.response=f,e.onSuccess(f,h,n.value),e.onChange(h,n.value))},u=f=>{const s={name:f.name,percentage:0,status:"ready",size:f.size,raw:f,uid:f.uid};if(e.listType==="picture-card"||e.listType==="picture")try{s.url=URL.createObjectURL(f)}catch(h){h.message,e.onError(h,s,n.value)}n.value=[...n.value,s],e.onChange(s,n.value)},d=async f=>{const s=f instanceof File?r(f):f;s||Tn(R0,"file to be removed not found");const h=v=>{i(v);const m=n.value;m.splice(m.indexOf(v),1),e.onRemove(v,m),TV(v)};e.beforeRemove?await e.beforeRemove(s,n.value)!==!1&&h(s):h(s)};function c(){n.value.filter(({status:f})=>f==="ready").forEach(({raw:f})=>{var s;return f&&((s=t.value)==null?void 0:s.upload(f))})}return ke(()=>e.listType,f=>{f!=="picture-card"&&f!=="picture"||(n.value=n.value.map(s=>{const{raw:h,url:v}=s;if(!v&&h)try{s.url=URL.createObjectURL(h)}catch(m){e.onError(m,s,n.value)}return s}))}),ke(n,f=>{for(const s of f)s.uid||(s.uid=Pw()),s.status||(s.status="success")},{immediate:!0,deep:!0}),{uploadFiles:n,abort:i,clearFiles:l,handleError:g,handleProgress:o,handleStart:u,handleSuccess:a,handleRemove:d,submit:c}},OV={name:"ElUpload"},AV=Ee({...OV,props:aV,setup(e,{expose:t}){const n=e,r=Ho(),i=Ja(),l=gn(),{abort:g,submit:o,clearFiles:a,uploadFiles:u,handleStart:d,handleError:c,handleRemove:f,handleSuccess:s,handleProgress:h}=EV(n,l),v=ee(()=>n.listType==="picture-card"),m=ee(()=>({...n,fileList:u.value,onStart:d,onProgress:h,onSuccess:s,onError:c,onRemove:f}));return Xt(()=>{u.value.forEach(({url:y})=>{y!=null&&y.startsWith("blob:")&&URL.revokeObjectURL(y)})}),gt(A1,{accept:Rn(n,"accept")}),t({abort:g,submit:o,clearFiles:a,handleStart:d,handleRemove:f}),(y,b)=>(z(),se("div",null,[C(v)&&y.showFileList?(z(),be(M0,{key:0,disabled:C(i),"list-type":y.listType,files:C(u),"handle-preview":y.onPreview,onRemove:C(f)},sc({append:he(()=>[le(L0,In({ref_key:"uploadRef",ref:l},C(m)),{default:he(()=>[C(r).trigger?xe(y.$slots,"trigger",{key:0}):ye("v-if",!0),!C(r).trigger&&C(r).default?xe(y.$slots,"default",{key:1}):ye("v-if",!0)]),_:3},16)]),_:2},[y.$slots.file?{name:"default",fn:he(({file:_})=>[xe(y.$slots,"file",{file:_})])}:void 0]),1032,["disabled","list-type","files","handle-preview","onRemove"])):ye("v-if",!0),!C(v)||C(v)&&!y.showFileList?(z(),be(L0,In({key:1,ref_key:"uploadRef",ref:l},C(m)),{default:he(()=>[C(r).trigger?xe(y.$slots,"trigger",{key:0}):ye("v-if",!0),!C(r).trigger&&C(r).default?xe(y.$slots,"default",{key:1}):ye("v-if",!0)]),_:3},16)):ye("v-if",!0),y.$slots.trigger?xe(y.$slots,"default",{key:2}):ye("v-if",!0),xe(y.$slots,"tip"),!C(v)&&y.showFileList?(z(),be(M0,{key:3,disabled:C(i),"list-type":y.listType,files:C(u),"handle-preview":y.onPreview,onRemove:C(f)},sc({_:2},[y.$slots.file?{name:"default",fn:he(({file:_})=>[xe(y.$slots,"file",{file:_})])}:void 0]),1032,["disabled","list-type","files","handle-preview","onRemove"])):ye("v-if",!0)]))}});var PV=Le(AV,[["__file","/home/runner/work/element-plus/element-plus/packages/components/upload/src/upload.vue"]]);const Jp=kt(PV),Mw=["success","info","warning","error"],On=sn({customClass:"",center:!1,dangerouslyUseHTMLString:!1,duration:3e3,icon:void 0,id:"",message:"",onClose:void 0,showClose:!1,type:"info",offset:16,zIndex:0,grouping:!1,repeatNum:1,appendTo:dt?document.body:void 0}),IV=Fe({customClass:{type:String,default:On.customClass},center:{type:Boolean,default:On.center},dangerouslyUseHTMLString:{type:Boolean,default:On.dangerouslyUseHTMLString},duration:{type:Number,default:On.duration},icon:{type:Wt,default:On.icon},id:{type:String,default:On.id},message:{type:Pe([String,Object,Function]),default:On.message},onClose:{type:Pe(Function),required:!1},showClose:{type:Boolean,default:On.showClose},type:{type:String,values:Mw,default:On.type},offset:{type:Number,default:On.offset},zIndex:{type:Number,default:On.zIndex},grouping:{type:Boolean,default:On.grouping},repeatNum:{type:Number,default:On.repeatNum}}),MV={destroy:()=>!0},Ir=lb([]),LV=e=>{const t=Ir.findIndex(i=>i.id===e),n=Ir[t];let r;return t>0&&(r=Ir[t-1]),{current:n,prev:r}},RV=e=>{const{prev:t}=LV(e);return t?t.vm.exposed.bottom.value:0},DV=["id"],$V=["innerHTML"],BV={name:"ElMessage"},FV=Ee({...BV,props:IV,emits:MV,setup(e,{expose:t}){const n=e,{Close:r}=g1,i=Ie("message"),l=oe(),g=oe(!1),o=oe(0);let a;const u=ee(()=>n.type?n.type==="error"?"danger":n.type:"info"),d=ee(()=>{const w=n.type;return{[i.bm("icon",w)]:w&&vc[w]}}),c=ee(()=>n.icon||vc[n.type]||""),f=ee(()=>RV(n.id)),s=ee(()=>n.offset+f.value),h=ee(()=>o.value+s.value),v=ee(()=>({top:`${s.value}px`,zIndex:n.zIndex}));function m(){n.duration!==0&&({stop:a}=il(()=>{b()},n.duration))}function y(){a==null||a()}function b(){g.value=!1}function _({code:w}){w===Ot.esc&&b()}return rt(()=>{m(),g.value=!0}),ke(()=>n.repeatNum,()=>{y(),m()}),St(document,"keydown",_),Nr(l,()=>{o.value=l.value.getBoundingClientRect().height}),t({visible:g,bottom:h,close:b}),(w,S)=>(z(),be(vn,{name:C(i).b("fade"),onBeforeLeave:w.onClose,onAfterLeave:S[0]||(S[0]=x=>w.$emit("destroy")),persisted:""},{default:he(()=>[Et(ne("div",{id:w.id,ref_key:"messageRef",ref:l,class:de([C(i).b(),{[C(i).m(w.type)]:w.type&&!w.icon},C(i).is("center",w.center),C(i).is("closable",w.showClose),w.customClass]),style:ze(C(v)),role:"alert",onMouseenter:y,onMouseleave:m},[w.repeatNum>1?(z(),be(C(WD),{key:0,value:w.repeatNum,type:C(u),class:de(C(i).e("badge"))},null,8,["value","type","class"])):ye("v-if",!0),C(c)?(z(),be(C(Me),{key:1,class:de([C(i).e("icon"),C(d)])},{default:he(()=>[(z(),be(Ct(C(c))))]),_:1},8,["class"])):ye("v-if",!0),xe(w.$slots,"default",{},()=>[w.dangerouslyUseHTMLString?(z(),se(Ne,{key:1},[ye(" Caution here, message could've been compromised, never use user's input as message "),ne("p",{class:de(C(i).e("content")),innerHTML:w.message},null,10,$V)],2112)):(z(),se("p",{key:0,class:de(C(i).e("content"))},Ae(w.message),3))]),w.showClose?(z(),be(C(Me),{key:2,class:de(C(i).e("closeBtn")),onClick:tt(b,["stop"])},{default:he(()=>[le(C(r))]),_:1},8,["class","onClick"])):ye("v-if",!0)],46,DV),[[Ht,g.value]])]),_:3},8,["name","onBeforeLeave"]))}});var zV=Le(FV,[["__file","/home/runner/work/element-plus/element-plus/packages/components/message/src/message.vue"]]);let NV=1;const Lw=e=>{const t=!e||Xe(e)||Yi(e)||Ue(e)?{message:e}:e,n={...On,...t};if(!n.appendTo)n.appendTo=document.body;else if(Xe(n.appendTo)){let r=document.querySelector(n.appendTo);$a(r)||(r=document.body),n.appendTo=r}return n},jV=e=>{const t=Ir.indexOf(e);if(t===-1)return;Ir.splice(t,1);const{handler:n}=e;n.close()},VV=({appendTo:e,...t},n)=>{const{nextZIndex:r}=Bl(),i=`message_${NV++}`,l=t.onClose,g=document.createElement("div"),o={...t,zIndex:r()+t.zIndex,id:i,onClose:()=>{l==null||l(),jV(c)},onDestroy:()=>{fc(null,g)}},a=le(zV,o,Ue(o.message)||Yi(o.message)?{default:Ue(o.message)?o.message:()=>o.message}:null);a.appContext=n||No._context,fc(a,g),e.appendChild(g.firstElementChild);const u=a.component,c={id:i,vnode:a,vm:u,handler:{close:()=>{u.exposed.visible.value=!1}},props:a.component.props};return c},No=(e={},t)=>{if(!dt)return{close:()=>{}};if(ft(Jd.max)&&Ir.length>=Jd.max)return{close:()=>{}};const n=Lw(e);if(n.grouping&&Ir.length){const i=Ir.find(({vnode:l})=>{var g;return((g=l.props)==null?void 0:g.message)===n.message});if(i)return i.props.repeatNum+=1,i.props.type=n.type,i.handler}const r=VV(n,t);return Ir.push(r),r.handler};Mw.forEach(e=>{No[e]=(t={},n)=>{const r=Lw(t);return No({...r,type:e},n)}});function UV(e){for(const t of Ir)(!e||e===t.props.type)&&t.handler.close()}No.closeAll=UV;No._context=null;const qi=FM(No,"$message"),HV=Ee({name:"ElMessageBox",directives:{TrapFocus:y$},components:{ElButton:at,ElFocusTrap:Wp,ElInput:Qa,ElOverlay:pw,ElIcon:Me,...g1},inheritAttrs:!1,props:{buttonSize:{type:String,validator:nf},modal:{type:Boolean,default:!0},lockScroll:{type:Boolean,default:!0},showClose:{type:Boolean,default:!0},closeOnClickModal:{type:Boolean,default:!0},closeOnPressEscape:{type:Boolean,default:!0},closeOnHashChange:{type:Boolean,default:!0},center:Boolean,draggable:Boolean,roundButton:{default:!1,type:Boolean},container:{type:String,default:"body"},boxType:{type:String,default:""}},emits:["vanish","action"],setup(e,{emit:t}){const{t:n}=yn(),r=Ie("message-box"),i=oe(!1),{nextZIndex:l}=Bl(),g=vt({autofocus:!0,beforeClose:null,callback:null,cancelButtonText:"",cancelButtonClass:"",confirmButtonText:"",confirmButtonClass:"",customClass:"",customStyle:{},dangerouslyUseHTMLString:!1,distinguishCancelAndClose:!1,icon:"",inputPattern:null,inputPlaceholder:"",inputType:"text",inputValue:null,inputValidator:null,inputErrorMessage:"",message:null,modalFade:!0,modalClass:"",showCancelButton:!1,showConfirmButton:!0,type:"",title:void 0,showInput:!1,action:"",confirmButtonLoading:!1,cancelButtonLoading:!1,confirmButtonDisabled:!1,editorErrorMessage:"",validateError:!1,zIndex:l()}),o=ee(()=>{const F=g.type;return{[r.bm("icon",F)]:F&&vc[F]}}),a=Gi(),u=Gi(),d=sr(ee(()=>e.buttonSize),{prop:!0,form:!0,formItem:!0}),c=ee(()=>g.icon||vc[g.type]||""),f=ee(()=>!!g.message),s=oe(),h=oe(),v=oe(),m=oe(),y=oe(),b=ee(()=>g.confirmButtonClass);ke(()=>g.inputValue,async F=>{await He(),e.boxType==="prompt"&&F!==null&&I()},{immediate:!0}),ke(()=>i.value,F=>{var O,V;F&&(e.boxType!=="prompt"&&(g.autofocus?v.value=(V=(O=y.value)==null?void 0:O.$el)!=null?V:s.value:v.value=s.value),g.zIndex=l()),e.boxType==="prompt"&&(F?He().then(()=>{var re;m.value&&m.value.$el&&(g.autofocus?v.value=(re=D())!=null?re:s.value:v.value=s.value)}):(g.editorErrorMessage="",g.validateError=!1))});const _=ee(()=>e.draggable);M1(s,h,_),rt(async()=>{await He(),e.closeOnHashChange&&window.addEventListener("hashchange",w)}),Xt(()=>{e.closeOnHashChange&&window.removeEventListener("hashchange",w)});function w(){!i.value||(i.value=!1,He(()=>{g.action&&t("action",g.action)}))}const S=()=>{e.closeOnClickModal&&E(g.distinguishCancelAndClose?"close":"cancel")},x=$p(S),k=F=>{if(g.inputType!=="textarea")return F.preventDefault(),E("confirm")},E=F=>{var O;e.boxType==="prompt"&&F==="confirm"&&!I()||(g.action=F,g.beforeClose?(O=g.beforeClose)==null||O.call(g,F,g,w):w())},I=()=>{if(e.boxType==="prompt"){const F=g.inputPattern;if(F&&!F.test(g.inputValue||""))return g.editorErrorMessage=g.inputErrorMessage||n("el.messagebox.error"),g.validateError=!0,!1;const O=g.inputValidator;if(typeof O=="function"){const V=O(g.inputValue);if(V===!1)return g.editorErrorMessage=g.inputErrorMessage||n("el.messagebox.error"),g.validateError=!0,!1;if(typeof V=="string")return g.editorErrorMessage=V,g.validateError=!0,!1}}return g.editorErrorMessage="",g.validateError=!1,!0},D=()=>{const F=m.value.$refs;return F.input||F.textarea},N=()=>{E("close")},L=()=>{e.closeOnPressEscape&&N()};return e.lockScroll&&L1(i),oL(i),{...rr(g),ns:r,overlayEvent:x,visible:i,hasMessage:f,typeClass:o,contentId:a,inputId:u,btnSize:d,iconComponent:c,confirmButtonClasses:b,rootRef:s,focusStartRef:v,headerRef:h,inputRef:m,confirmRef:y,doClose:w,handleClose:N,onCloseRequested:L,handleWrapperClick:S,handleInputEnter:k,handleAction:E,t:n}}}),WV=["aria-label","aria-describedby"],YV=["aria-label"],XV=["id"];function KV(e,t,n,r,i,l){const g=mt("el-icon"),o=mt("close"),a=mt("el-input"),u=mt("el-button"),d=mt("el-focus-trap"),c=mt("el-overlay");return z(),be(vn,{name:"fade-in-linear",onAfterLeave:t[11]||(t[11]=f=>e.$emit("vanish")),persisted:""},{default:he(()=>[Et(le(c,{"z-index":e.zIndex,"overlay-class":[e.ns.is("message-box"),e.modalClass],mask:e.modal},{default:he(()=>[ne("div",{role:"dialog","aria-label":e.title,"aria-modal":"true","aria-describedby":e.showInput?void 0:e.contentId,class:de(`${e.ns.namespace.value}-overlay-message-box`),onClick:t[8]||(t[8]=(...f)=>e.overlayEvent.onClick&&e.overlayEvent.onClick(...f)),onMousedown:t[9]||(t[9]=(...f)=>e.overlayEvent.onMousedown&&e.overlayEvent.onMousedown(...f)),onMouseup:t[10]||(t[10]=(...f)=>e.overlayEvent.onMouseup&&e.overlayEvent.onMouseup(...f))},[le(d,{loop:"",trapped:e.visible,"focus-trap-el":e.rootRef,"focus-start-el":e.focusStartRef,onReleaseRequested:e.onCloseRequested},{default:he(()=>[ne("div",{ref:"rootRef",class:de([e.ns.b(),e.customClass,e.ns.is("draggable",e.draggable),{[e.ns.m("center")]:e.center}]),style:ze(e.customStyle),tabindex:"-1",onClick:t[7]||(t[7]=tt(()=>{},["stop"]))},[e.title!==null&&e.title!==void 0?(z(),se("div",{key:0,ref:"headerRef",class:de(e.ns.e("header"))},[ne("div",{class:de(e.ns.e("title"))},[e.iconComponent&&e.center?(z(),be(g,{key:0,class:de([e.ns.e("status"),e.typeClass])},{default:he(()=>[(z(),be(Ct(e.iconComponent)))]),_:1},8,["class"])):ye("v-if",!0),ne("span",null,Ae(e.title),1)],2),e.showClose?(z(),se("button",{key:0,type:"button",class:de(e.ns.e("headerbtn")),"aria-label":e.t("el.messagebox.close"),onClick:t[0]||(t[0]=f=>e.handleAction(e.distinguishCancelAndClose?"close":"cancel")),onKeydown:t[1]||(t[1]=$t(tt(f=>e.handleAction(e.distinguishCancelAndClose?"close":"cancel"),["prevent"]),["enter"]))},[le(g,{class:de(e.ns.e("close"))},{default:he(()=>[le(o)]),_:1},8,["class"])],42,YV)):ye("v-if",!0)],2)):ye("v-if",!0),ne("div",{id:e.contentId,class:de(e.ns.e("content"))},[ne("div",{class:de(e.ns.e("container"))},[e.iconComponent&&!e.center&&e.hasMessage?(z(),be(g,{key:0,class:de([e.ns.e("status"),e.typeClass])},{default:he(()=>[(z(),be(Ct(e.iconComponent)))]),_:1},8,["class"])):ye("v-if",!0),e.hasMessage?(z(),se("div",{key:1,class:de(e.ns.e("message"))},[xe(e.$slots,"default",{},()=>[e.dangerouslyUseHTMLString?(z(),be(Ct(e.showInput?"label":"p"),{key:1,for:e.showInput?e.inputId:void 0,innerHTML:e.message},null,8,["for","innerHTML"])):(z(),be(Ct(e.showInput?"label":"p"),{key:0,for:e.showInput?e.inputId:void 0},{default:he(()=>[je(Ae(e.dangerouslyUseHTMLString?"":e.message),1)]),_:1},8,["for"]))])],2)):ye("v-if",!0)],2),Et(ne("div",{class:de(e.ns.e("input"))},[le(a,{id:e.inputId,ref:"inputRef",modelValue:e.inputValue,"onUpdate:modelValue":t[2]||(t[2]=f=>e.inputValue=f),type:e.inputType,placeholder:e.inputPlaceholder,"aria-invalid":e.validateError,class:de({invalid:e.validateError}),onKeydown:$t(e.handleInputEnter,["enter"])},null,8,["id","modelValue","type","placeholder","aria-invalid","class","onKeydown"]),ne("div",{class:de(e.ns.e("errormsg")),style:ze({visibility:e.editorErrorMessage?"visible":"hidden"})},Ae(e.editorErrorMessage),7)],2),[[Ht,e.showInput]])],10,XV),ne("div",{class:de(e.ns.e("btns"))},[e.showCancelButton?(z(),be(u,{key:0,loading:e.cancelButtonLoading,class:de([e.cancelButtonClass]),round:e.roundButton,size:e.btnSize,onClick:t[3]||(t[3]=f=>e.handleAction("cancel")),onKeydown:t[4]||(t[4]=$t(tt(f=>e.handleAction("cancel"),["prevent"]),["enter"]))},{default:he(()=>[je(Ae(e.cancelButtonText||e.t("el.messagebox.cancel")),1)]),_:1},8,["loading","class","round","size"])):ye("v-if",!0),Et(le(u,{ref:"confirmRef",type:"primary",loading:e.confirmButtonLoading,class:de([e.confirmButtonClasses]),round:e.roundButton,disabled:e.confirmButtonDisabled,size:e.btnSize,onClick:t[5]||(t[5]=f=>e.handleAction("confirm")),onKeydown:t[6]||(t[6]=$t(tt(f=>e.handleAction("confirm"),["prevent"]),["enter"]))},{default:he(()=>[je(Ae(e.confirmButtonText||e.t("el.messagebox.confirm")),1)]),_:1},8,["loading","class","round","disabled","size"]),[[Ht,e.showConfirmButton]])],2)],6)]),_:3},8,["trapped","focus-trap-el","focus-start-el","onReleaseRequested"])],42,WV)]),_:3},8,["z-index","overlay-class","mask"]),[[Ht,e.visible]])]),_:3})}var GV=Le(HV,[["render",KV],["__file","/home/runner/work/element-plus/element-plus/packages/components/message-box/src/index.vue"]]);const fl=new Map,qV=(e,t,n=null)=>{const r=le(GV,e,Ue(e.message)||Yi(e.message)?{default:Ue(e.message)?e.message:()=>e.message}:null);return r.appContext=n,fc(r,t),document.body.appendChild(t.firstElementChild),r.component},ZV=()=>document.createElement("div"),JV=(e,t)=>{const n=ZV();e.onVanish=()=>{fc(null,n),fl.delete(i)},e.onAction=l=>{const g=fl.get(i);let o;e.showInput?o={value:i.inputValue,action:l}:o=l,e.callback?e.callback(o,r.proxy):l==="cancel"||l==="close"?e.distinguishCancelAndClose&&l!=="cancel"?g.reject("close"):g.reject("cancel"):g.resolve(o)};const r=qV(e,n,t),i=r.proxy;for(const l in e)nt(e,l)&&!nt(i.$props,l)&&(i[l]=e[l]);return i.visible=!0,i};function qo(e,t=null){if(!dt)return Promise.reject();let n;return Xe(e)||Yi(e)?e={message:e}:n=e.callback,new Promise((r,i)=>{const l=JV(e,t!=null?t:qo._context);fl.set(l,{options:e,callback:n,resolve:r,reject:i})})}const QV=["alert","confirm","prompt"],e7={alert:{closeOnPressEscape:!1,closeOnClickModal:!1},confirm:{showCancelButton:!0},prompt:{showCancelButton:!0,showInput:!0}};QV.forEach(e=>{qo[e]=t7(e)});function t7(e){return(t,n,r,i)=>{let l="";return ot(n)?(r=n,l=""):pr(n)?l="":l=n,qo(Object.assign({title:l,message:t,type:"",...e7[e]},r,{boxType:e}),i)}}qo.close=()=>{fl.forEach((e,t)=>{t.doClose()}),fl.clear()};qo._context=null;const Ai=qo;Ai.install=e=>{Ai._context=e._context,e.config.globalProperties.$msgbox=Ai,e.config.globalProperties.$messageBox=Ai,e.config.globalProperties.$alert=Ai.alert,e.config.globalProperties.$confirm=Ai.confirm,e.config.globalProperties.$prompt=Ai.prompt};const Rw=Ai;const Rt=Wo("options",()=>{const e=fn("options",oe({colorMode:Mv({emitAuto:!0})})),t=fn("pageSize",25),n=fn("pageless","Disabled"),r=fn("allowLargerParams","Disabled"),i=fn("alsoRequestAvi","Enabled"),l=fn("keepImageGenOnDisconnect","Disabled"),g=fn("autoCarousel","Enabled"),o=fn("useBeta","Disabled"),a=fn("imageDownloadType","PNG"),u=fn("imageResizeMode","NoScale"),d=fn("baseURL","");return e.value.colorMode=Mv({emitAuto:!0,initialValue:e.value.colorMode}),{options:e,pageSize:t,pageless:n,allowLargerParams:r,alsoRequestAvi:i,keepImageGenOnDisconnect:l,autoCarousel:g,useBeta:o,imageDownloadType:a,imageResizeMode:u,baseURL:d}});/*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/ggml/src/ggml-cpu/arch/arm/cpu-feats.cpp b/ggml/src/ggml-cpu/arch/arm/cpu-feats.cpp index adfbd2e4e..a64ad7a3c 100644 --- a/ggml/src/ggml-cpu/arch/arm/cpu-feats.cpp +++ b/ggml/src/ggml-cpu/arch/arm/cpu-feats.cpp @@ -8,6 +8,22 @@ #include #endif +#if !defined(HWCAP_FPHP) +#define HWCAP_FPHP (1 << 9) +#endif + +#if !defined(HWCAP_ASIMDHP) +#define HWCAP_ASIMDHP (1 << 10) +#endif + +#if !defined(HWCAP_ASIMDDP) +#define HWCAP_ASIMDDP (1 << 20) +#endif + +#if !defined(HWCAP_SVE) +#define HWCAP_SVE (1 << 22) +#endif + #if !defined(HWCAP2_SVE2) #define HWCAP2_SVE2 (1 << 1) #endif @@ -23,7 +39,7 @@ struct aarch64_features { // has_neon not needed, aarch64 has NEON guaranteed bool has_dotprod = false; - bool has_fp16_va = false; + bool has_fp16 = false; bool has_sve = false; bool has_sve2 = false; bool has_i8mm = false; @@ -36,7 +52,7 @@ struct aarch64_features { uint32_t hwcap2 = getauxval(AT_HWCAP2); has_dotprod = !!(hwcap & HWCAP_ASIMDDP); - has_fp16_va = !!(hwcap & HWCAP_FPHP); + has_fp16 = !!(hwcap & HWCAP_FPHP) && !!(hwcap & HWCAP_ASIMDHP); has_sve = !!(hwcap & HWCAP_SVE); has_sve2 = !!(hwcap2 & HWCAP2_SVE2); has_i8mm = !!(hwcap2 & HWCAP2_I8MM); @@ -75,7 +91,7 @@ static int ggml_backend_cpu_aarch64_score() { score += 1<<1; #endif #ifdef GGML_USE_FP16_VECTOR_ARITHMETIC - if (!af.has_fp16_va) { return 0; } + if (!af.has_fp16) { return 0; } score += 1<<2; #endif #ifdef GGML_USE_SVE diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 3249f1f4f..ba73cf9b8 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -5222,6 +5222,7 @@ static bool ggml_backend_cuda_device_offload_op(ggml_backend_dev_t dev, const gg static ggml_backend_event_t ggml_backend_cuda_device_event_new(ggml_backend_dev_t dev) { #ifdef GGML_CUDA_NO_PEER_COPY + GGML_UNUSED(dev); return nullptr; #else ggml_backend_cuda_device_context * dev_ctx = (ggml_backend_cuda_device_context *)dev->context; diff --git a/ggml/src/ggml-cuda/quantize.cu b/ggml/src/ggml-cuda/quantize.cu index 2bd9b6262..bcc772395 100644 --- a/ggml/src/ggml-cuda/quantize.cu +++ b/ggml/src/ggml-cuda/quantize.cu @@ -8,7 +8,6 @@ struct __builtin_align__(32) float8 { float x; float y; float z; float w; float p; float q; float r; float s; }; -#endif #if CUDART_VERSION >= 12080 static __device__ __forceinline__ float nvfp4_native_scale_error( @@ -49,6 +48,7 @@ static __device__ __forceinline__ float nvfp4_native_scale_error( return err; } #endif // CUDART_VERSION >= 12080 +#endif // defined(BLACKWELL_MMA_AVAILABLE) __launch_bounds__(CUDA_QUANTIZE_BLOCK_SIZE, 1) static __global__ void quantize_q8_1( diff --git a/ggml/src/ggml-metal/ggml-metal-ops.cpp b/ggml/src/ggml-metal/ggml-metal-ops.cpp index c5d7619c1..6d324056d 100644 --- a/ggml/src/ggml-metal/ggml-metal-ops.cpp +++ b/ggml/src/ggml-metal/ggml-metal-ops.cpp @@ -3816,7 +3816,7 @@ int ggml_metal_op_norm(ggml_metal_op_t ctx, int idx) { } nth = std::min(nth, ggml_metal_pipeline_max_theads_per_threadgroup(pipeline)); - nth = std::min(nth, args.ne00_t); + nth = std::min(nth, (args.ne00_t + 31)/32*32); const size_t smem = pipeline.smem; diff --git a/ggml/src/ggml-metal/ggml-metal.metal b/ggml/src/ggml-metal/ggml-metal.metal index 7d12cb0fe..92258b737 100644 --- a/ggml/src/ggml-metal/ggml-metal.metal +++ b/ggml/src/ggml-metal/ggml-metal.metal @@ -11328,8 +11328,8 @@ kernel void kernel_lightning_indexer( const int i_kv_0 = tgpig.x*NK; // first key of this threadgroup const int i_kv = i_kv_0 + sgitg*NKPSG; // first key of this simdgroup - threadgroup half4x4 sk4x4[NK*DK16]; - threadgroup half * sk = (threadgroup half *) sk4x4; + threadgroup half sk[NK * DK16 * 16]; + threadgroup half4x4 * sk4x4 = (threadgroup half4x4 *) sk; for (short i = tiitg; i < NK*DK16; i += NTG) { const short ik = i/DK16; diff --git a/ggml/src/ggml-sycl/dsv4-hc.cpp b/ggml/src/ggml-sycl/dsv4-hc.cpp new file mode 100644 index 000000000..bb66e8c1b --- /dev/null +++ b/ggml/src/ggml-sycl/dsv4-hc.cpp @@ -0,0 +1,280 @@ +#include "ggml-impl.h" +#include "dsv4-hc.hpp" + +#include + +static constexpr int DSV4_HC = 4; + +static void dsv4_hc_pre_f32_sycl( + const float * x, const float * weights, float * dst, + int64_t n_embd, int64_t hc, int64_t n_tokens, + int64_t sx0, int64_t sx1, int64_t sx2, + int64_t sw0, int64_t sw1, + int64_t sd0, int64_t sd1, + queue_ptr stream) { + const int64_t nr = n_embd * n_tokens; + const int64_t block_size = 256; + const int64_t num_blocks = (nr + block_size - 1) / block_size; + + stream->parallel_for( + sycl::nd_range<1>(sycl::range<1>(num_blocks * block_size), sycl::range<1>(block_size)), + [=](sycl::nd_item<1> item) { + const int64_t ir = item.get_global_id(0); + if (ir >= nr) { + return; + } + + const int64_t i0 = ir % n_embd; + const int64_t it = ir / n_embd; + + float sum = x[i0*sx0 + it*sx2] * weights[it*sw1]; + for (int64_t ih = 1; ih < hc; ++ih) { + const float xv = x[i0*sx0 + ih*sx1 + it*sx2]; + const float wv = weights[ih*sw0 + it*sw1]; + sum += xv * wv; + } + + dst[i0*sd0 + it*sd1] = sum; + }); +} + +static void dsv4_hc_comb_norm_cols(float * comb, float eps) { + for (int idst = 0; idst < DSV4_HC; ++idst) { + float sum = eps; + for (int isrc = 0; isrc < DSV4_HC; ++isrc) { + sum += comb[idst + DSV4_HC*isrc]; + } + + const float inv_sum = 1.0f / sum; + for (int isrc = 0; isrc < DSV4_HC; ++isrc) { + comb[idst + DSV4_HC*isrc] *= inv_sum; + } + } +} + +static void dsv4_hc_comb_norm_rows(float * comb, float eps) { + for (int isrc = 0; isrc < DSV4_HC; ++isrc) { + float sum = eps; + for (int idst = 0; idst < DSV4_HC; ++idst) { + sum += comb[idst + DSV4_HC*isrc]; + } + + const float inv_sum = 1.0f / sum; + for (int idst = 0; idst < DSV4_HC; ++idst) { + comb[idst + DSV4_HC*isrc] *= inv_sum; + } + } +} + +static void dsv4_hc_comb_f32_sycl( + const float * mixes, + const float * scale, + const float * base, + float * dst, + int64_t n_tokens, + int64_t sm0, + int64_t sm1, + int64_t ss0, + int64_t sb0, + int64_t sd0, + int64_t sd1, + int64_t sd2, + float eps, + int32_t n_iter, + queue_ptr stream) { + constexpr int comb_offset = 2*DSV4_HC; + + const int64_t block_size = 256; + const int64_t num_blocks = (n_tokens + block_size - 1) / block_size; + + stream->parallel_for( + sycl::nd_range<1>(sycl::range<1>(num_blocks * block_size), sycl::range<1>(block_size)), + [=](sycl::nd_item<1> item_ct1) { + const int64_t it = item_ct1.get_global_id(0); + + if (it >= n_tokens) { + return; + } + + const float scale_comb = scale[2*ss0]; + float comb[DSV4_HC*DSV4_HC]; + + for (int isrc = 0; isrc < DSV4_HC; ++isrc) { + float max = -INFINITY; + for (int idst = 0; idst < DSV4_HC; ++idst) { + const int idx = idst + DSV4_HC*isrc; + const float v = mixes[(comb_offset + idx)*sm0 + it*sm1] * scale_comb + base[(comb_offset + idx)*sb0]; + comb[idx] = v; + max = fmaxf(max, v); + } + + float sum = 0.0f; + for (int idst = 0; idst < DSV4_HC; ++idst) { + const int idx = idst + DSV4_HC*isrc; + const float v = expf(comb[idx] - max); + comb[idx] = v; + sum += v; + } + + const float inv_sum = 1.0f / sum; + for (int idst = 0; idst < DSV4_HC; ++idst) { + const int idx = idst + DSV4_HC*isrc; + comb[idx] = comb[idx] * inv_sum + eps; + } + } + + dsv4_hc_comb_norm_cols(comb, eps); + for (int32_t i = 1; i < n_iter; ++i) { + dsv4_hc_comb_norm_rows(comb, eps); + dsv4_hc_comb_norm_cols(comb, eps); + } + + for (int isrc = 0; isrc < DSV4_HC; ++isrc) { + for (int idst = 0; idst < DSV4_HC; ++idst) { + const int idx = idst + DSV4_HC*isrc; + dst[idst*sd0 + isrc*sd1 + it*sd2] = comb[idx]; + } + } + }); +} + +static void dsv4_hc_post_f32_sycl( + const float * x, const float * residual, const float * post, const float * comb, float * dst, + int64_t n_embd, int64_t hc, int64_t n_tokens, + int64_t sx0, int64_t sx1, + int64_t sr0, int64_t sr1, int64_t sr2, + int64_t sp0, int64_t sp1, + int64_t sc0, int64_t sc1, int64_t sc2, + int64_t sd0, int64_t sd1, int64_t sd2, + queue_ptr stream) { + const int64_t nr = n_embd * hc * n_tokens; + const int64_t block_size = 256; + const int64_t num_blocks = (nr + block_size - 1) / block_size; + + stream->parallel_for( + sycl::nd_range<1>(sycl::range<1>(num_blocks * block_size), sycl::range<1>(block_size)), + [=](sycl::nd_item<1> item) { + const int64_t ir = item.get_global_id(0); + if (ir >= nr) { + return; + } + + const int64_t i0 = ir % n_embd; + const int64_t idst = (ir / n_embd) % hc; + const int64_t it = ir / (n_embd * hc); + + float sum = x[i0*sx0 + it*sx1] * post[idst*sp0 + it*sp1]; + for (int64_t isrc = 0; isrc < hc; ++isrc) { + sum += residual[i0*sr0 + isrc*sr1 + it*sr2] * comb[idst*sc0 + isrc*sc1 + it*sc2]; + } + + dst[i0*sd0 + idst*sd1 + it*sd2] = sum; + }); +} + +void ggml_sycl_op_dsv4_hc_pre(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { + scope_op_debug_print scope_dbg_print(__func__, dst, /*num_src=*/2); + const ggml_tensor * x = dst->src[0]; + const ggml_tensor * weights = dst->src[1]; + + GGML_ASSERT(x->type == GGML_TYPE_F32); + GGML_ASSERT(weights->type == GGML_TYPE_F32); + GGML_ASSERT(dst->type == GGML_TYPE_F32); + + GGML_TENSOR_LOCALS(size_t, nbx, x, nb); + GGML_TENSOR_LOCALS(size_t, nbw, weights, nb); + GGML_TENSOR_LOCALS(size_t, nbd, dst, nb); + + const int64_t n_embd = x->ne[0]; + const int64_t hc = x->ne[1]; + const int64_t n_tokens = x->ne[2]; + + queue_ptr stream = ctx.stream(); + + dsv4_hc_pre_f32_sycl( + (const float *) x->data, (const float *) weights->data, (float *) dst->data, + n_embd, hc, n_tokens, + nbx0 / sizeof(float), nbx1 / sizeof(float), nbx2 / sizeof(float), + nbw0 / sizeof(float), nbw1 / sizeof(float), + nbd0 / sizeof(float), nbd1 / sizeof(float), + stream); +} + +void ggml_sycl_op_dsv4_hc_comb(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { + scope_op_debug_print scope_dbg_print(__func__, dst, /*num_src=*/3); + + const ggml_tensor * mixes = dst->src[0]; + const ggml_tensor * scale = dst->src[1]; + const ggml_tensor * base = dst->src[2]; + + GGML_ASSERT(mixes->type == GGML_TYPE_F32); + GGML_ASSERT(scale->type == GGML_TYPE_F32); + GGML_ASSERT(base->type == GGML_TYPE_F32); + GGML_ASSERT(dst->type == GGML_TYPE_F32); + + constexpr int64_t hc_mix_dim = (2 + DSV4_HC)*DSV4_HC; + + GGML_ASSERT(mixes->ne[0] == hc_mix_dim); + GGML_ASSERT(dst->ne[0] == DSV4_HC); + GGML_ASSERT(dst->ne[1] == DSV4_HC); + GGML_ASSERT(dst->ne[2] == mixes->ne[1]); + GGML_ASSERT(scale->ne[0] >= 3); + GGML_ASSERT(base->ne[0] == hc_mix_dim); + + GGML_TENSOR_LOCALS(size_t, nbm, mixes, nb); + GGML_TENSOR_LOCALS(size_t, nbs, scale, nb); + GGML_TENSOR_LOCALS(size_t, nbb, base, nb); + GGML_TENSOR_LOCALS(size_t, nbd, dst, nb); + + const int64_t n_tokens = mixes->ne[1]; + const float eps = ggml_get_op_params_f32(dst, 0); + const int32_t n_iter = ggml_get_op_params_i32(dst, 1); + + queue_ptr stream = ctx.stream(); + + dsv4_hc_comb_f32_sycl( + (const float *) mixes->data, (const float *) scale->data, (const float *) base->data, (float *) dst->data, + n_tokens, + nbm0 / sizeof(float), nbm1 / sizeof(float), + nbs0 / sizeof(float), + nbb0 / sizeof(float), + nbd0 / sizeof(float), nbd1 / sizeof(float), nbd2 / sizeof(float), + eps, n_iter, stream); +} + +void ggml_sycl_op_dsv4_hc_post(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { + scope_op_debug_print scope_dbg_print(__func__, dst, /*num_src=*/4); + const ggml_tensor * x = dst->src[0]; + const ggml_tensor * residual = dst->src[1]; + const ggml_tensor * post = dst->src[2]; + const ggml_tensor * comb = dst->src[3]; + + GGML_ASSERT(x->type == GGML_TYPE_F32); + GGML_ASSERT(residual->type == GGML_TYPE_F32); + GGML_ASSERT(post->type == GGML_TYPE_F32); + GGML_ASSERT(comb->type == GGML_TYPE_F32); + GGML_ASSERT(dst->type == GGML_TYPE_F32); + + GGML_TENSOR_LOCALS(size_t, nbx, x, nb); + GGML_TENSOR_LOCALS(size_t, nbr, residual, nb); + GGML_TENSOR_LOCALS(size_t, nbp, post, nb); + GGML_TENSOR_LOCALS(size_t, nbc, comb, nb); + GGML_TENSOR_LOCALS(size_t, nbd, dst, nb); + + const int64_t n_embd = x->ne[0]; + const int64_t n_tokens = x->ne[1]; + const int64_t hc = residual->ne[1]; + + queue_ptr stream = ctx.stream(); + + dsv4_hc_post_f32_sycl( + (const float *) x->data, (const float *) residual->data, + (const float *) post->data, (const float *) comb->data, (float *) dst->data, + n_embd, hc, n_tokens, + nbx0 / sizeof(float), nbx1 / sizeof(float), + nbr0 / sizeof(float), nbr1 / sizeof(float), nbr2 / sizeof(float), + nbp0 / sizeof(float), nbp1 / sizeof(float), + nbc0 / sizeof(float), nbc1 / sizeof(float), nbc2 / sizeof(float), + nbd0 / sizeof(float), nbd1 / sizeof(float), nbd2 / sizeof(float), + stream); +} diff --git a/ggml/src/ggml-sycl/dsv4-hc.hpp b/ggml/src/ggml-sycl/dsv4-hc.hpp new file mode 100644 index 000000000..330518d8a --- /dev/null +++ b/ggml/src/ggml-sycl/dsv4-hc.hpp @@ -0,0 +1,10 @@ +#ifndef GGML_SYCL_DSV4_HC_HPP +#define GGML_SYCL_DSV4_HC_HPP + +#include "common.hpp" + +void ggml_sycl_op_dsv4_hc_pre(ggml_backend_sycl_context & ctx, ggml_tensor * dst); +void ggml_sycl_op_dsv4_hc_comb(ggml_backend_sycl_context & ctx, ggml_tensor * dst); +void ggml_sycl_op_dsv4_hc_post(ggml_backend_sycl_context & ctx, ggml_tensor * dst); + +#endif // GGML_SYCL_DSV4_HC_HPP diff --git a/ggml/src/ggml-sycl/lightning-indexer.cpp b/ggml/src/ggml-sycl/lightning-indexer.cpp new file mode 100644 index 000000000..823b713ca --- /dev/null +++ b/ggml/src/ggml-sycl/lightning-indexer.cpp @@ -0,0 +1,197 @@ +#include "lightning-indexer.hpp" +#include "dequantize.hpp" + +static void lightning_indexer_f32_sycl( + const char * q, const char * k, const char * w, const char * m, float * dst, + int64_t n_embd, int64_t n_head, int64_t n_batch, int64_t n_stream, int64_t n_kv, + int64_t nem3, + int64_t nbq1, int64_t nbq2, int64_t nbq3, + int64_t nbk2, int64_t nbk3, + int64_t nbw1, int64_t nbw3, + int64_t nbm1, int64_t nbm3, + int64_t nb1, int64_t nb3, + ggml_type k_type, + queue_ptr stream) { + + constexpr int64_t LANES = WARP_SIZE; + constexpr int64_t ELEMS_PER_LANE = 8; + constexpr int64_t ROWS_PER_BLOCK = 4; + constexpr int64_t BLOCK_SIZE = ROWS_PER_BLOCK * LANES; + + const int64_t n_rows = n_batch * n_stream * n_kv; + const int64_t n_blocks = (n_rows + ROWS_PER_BLOCK - 1) / ROWS_PER_BLOCK; + + stream->parallel_for( + sycl::nd_range<1>( + sycl::range<1>(n_blocks * BLOCK_SIZE), + sycl::range<1>(BLOCK_SIZE)), + [=](sycl::nd_item<1> item) [[sycl::reqd_sub_group_size(WARP_SIZE)]] { + const int64_t ir = item.get_global_id(0); + const int64_t lane = ir % LANES; + const int64_t row = ir / LANES; + if (row >= n_rows) { + return; + } + + const int64_t i_bs = row / n_kv; + const int64_t i_kv = row % n_kv; + const int64_t i_batch = i_bs / n_stream; + const int64_t i_stream = i_bs % n_stream; + + // load K row slice into registers (row is contiguous, nbk0 == type size) + const char * k_base = k + i_kv*nbk2 + i_stream*nbk3; + float k_local[ELEMS_PER_LANE]; + if (k_type == GGML_TYPE_F16) { + const sycl::half * k_row = (const sycl::half *) k_base; +#pragma unroll + for (int64_t j = 0; j < ELEMS_PER_LANE; ++j) { + k_local[j] = static_cast(k_row[lane*ELEMS_PER_LANE + j]); + } + } else if (k_type == GGML_TYPE_F32) { + const float * k_row = (const float *) k_base; +#pragma unroll + for (int64_t j = 0; j < ELEMS_PER_LANE; ++j) { + k_local[j] = k_row[lane*ELEMS_PER_LANE + j]; + } + } else { + const int64_t lane_base = lane * ELEMS_PER_LANE; + switch (k_type) { + case GGML_TYPE_BF16: { + const sycl::ext::oneapi::bfloat16 * k_row = (const sycl::ext::oneapi::bfloat16 *) k_base; +#pragma unroll + for (int64_t j = 0; j < ELEMS_PER_LANE; ++j) { + k_local[j] = static_cast(k_row[lane_base + j]); + } + } break; + case GGML_TYPE_Q4_0: + case GGML_TYPE_Q4_1: + case GGML_TYPE_Q5_0: + case GGML_TYPE_Q5_1: { +#pragma unroll + for (int64_t j = 0; j < ELEMS_PER_LANE; ++j) { + const int64_t idx = lane_base + j; + const int64_t ib = idx / QK4_0; + const int iqs = idx % (QK4_0/2); + dfloat2 kv; + if (k_type == GGML_TYPE_Q4_0) { + dequantize_q4_0(k_base, ib, iqs, kv); + } else if (k_type == GGML_TYPE_Q4_1) { + dequantize_q4_1(k_base, ib, iqs, kv); + } else if (k_type == GGML_TYPE_Q5_0) { + dequantize_q5_0(k_base, ib, iqs, kv); + } else { + dequantize_q5_1(k_base, ib, iqs, kv); + } + k_local[j] = (idx % QK4_0) < (QK4_0/2) ? static_cast(kv.x()) : static_cast(kv.y()); + } + } break; + case GGML_TYPE_Q8_0: { +#pragma unroll + for (int64_t pair = 0; pair < ELEMS_PER_LANE / 2; ++pair) { + const int64_t elem0 = lane_base + 2 * pair; + dfloat2 kv; + dequantize_q8_0(k_base, elem0 / QK8_0, elem0 % QK8_0, kv); + k_local[2 * pair + 0] = static_cast(kv.x()); + k_local[2 * pair + 1] = static_cast(kv.y()); + } + } break; + case GGML_TYPE_IQ4_NL: { +#pragma unroll + for (int64_t pair = 0; pair < ELEMS_PER_LANE / 2; ++pair) { + const int64_t elem0 = lane_base + 2 * pair; + dfloat2 kv; + dequantize_iq4_nl(k_base, elem0 / QK4_NL, elem0 % QK4_NL, kv); + k_local[2 * pair + 0] = static_cast(kv.x()); + k_local[2 * pair + 1] = static_cast(kv.y()); + } + } break; + default: +#pragma unroll + for (int64_t j = 0; j < ELEMS_PER_LANE; ++j) { + k_local[j] = 0.0f; + } + break; + } + } + + const char * q_base = q + i_batch*nbq2 + i_stream*nbq3; + const float * w_base = (const float *) (w + i_batch*nbw1 + i_stream*nbw3); + + float score = 0.0f; + for (int64_t h = 0; h < n_head; ++h) { + const float * q_row = (const float *) (q_base + h*nbq1); + float dot = 0.0f; +#pragma unroll + for (int64_t j = 0; j < ELEMS_PER_LANE; ++j) { + const int64_t i = lane*ELEMS_PER_LANE + j; + if (i < n_embd) { + dot += q_row[i] * k_local[j]; + } + } + dot = sycl::reduce_over_group(item.get_sub_group(), dot, sycl::plus()); + if (lane == 0) { + score += sycl::max(dot, 0.0f) * w_base[h]; + } + } + + if (lane == 0) { + const sycl::half * m_base = (const sycl::half *) (m + i_batch*nbm1 + (i_stream % nem3)*nbm3); + // flat-index store: storing through a strided base pointer + // hangs/misroutes writes on this stack when n_batch*n_stream > 1 + const int64_t dst_idx = i_kv + i_batch*(nb1/sizeof(float)) + i_stream*(nb3/sizeof(float)); + dst[dst_idx] = score + static_cast(m_base[i_kv]); + } + }); +} + +void ggml_sycl_op_lightning_indexer(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { + scope_op_debug_print scope_dbg_print(__func__, dst, /*num_src=*/4); + const ggml_tensor * q = dst->src[0]; + const ggml_tensor * k = dst->src[1]; + const ggml_tensor * w = dst->src[2]; // weights + const ggml_tensor * m = dst->src[3]; // mask + + GGML_ASSERT(dst->type == GGML_TYPE_F32); + GGML_ASSERT( q->type == GGML_TYPE_F32); + GGML_ASSERT( w->type == GGML_TYPE_F32); + GGML_ASSERT( m->type == GGML_TYPE_F16); + GGML_ASSERT(k->type == GGML_TYPE_F16 || k->type == GGML_TYPE_F32 || k->type == GGML_TYPE_BF16 || + k->type == GGML_TYPE_Q8_0 || k->type == GGML_TYPE_Q5_1 || k->type == GGML_TYPE_Q5_0 || + k->type == GGML_TYPE_Q4_1 || k->type == GGML_TYPE_Q4_0 || k->type == GGML_TYPE_IQ4_NL); + + GGML_TENSOR_LOCALS(int64_t, neq, q, ne); + GGML_TENSOR_LOCALS(size_t, nbq, q, nb); + GGML_TENSOR_LOCALS(int64_t, nek, k, ne); + GGML_TENSOR_LOCALS(size_t, nbk, k, nb); + GGML_TENSOR_LOCALS(size_t, nbw, w, nb); + GGML_TENSOR_LOCALS(int64_t, nem, m, ne); + GGML_TENSOR_LOCALS(size_t, nbm, m, nb); + GGML_TENSOR_LOCALS(int64_t, ne, dst, ne); + GGML_TENSOR_LOCALS(size_t, nb, dst, nb); + + // input rows must be contiguous + GGML_ASSERT(nbq0 == ggml_type_size(q->type)); + GGML_ASSERT(nbk0 == ggml_type_size(k->type)); + GGML_ASSERT(nbm0 == ggml_type_size(m->type)); + GGML_ASSERT(nb0 == ggml_type_size(dst->type)); + + const int64_t n_embd = neq0; + const int64_t n_head = neq1; + const int64_t n_batch = neq2; + const int64_t n_stream = neq3; + const int64_t n_kv = nek2; + + GGML_ASSERT(n_embd == WARP_SIZE * 8); + + lightning_indexer_f32_sycl( + (const char *) q->data, (const char *) k->data, + (const char *) w->data, (const char *) m->data, (float *) dst->data, + n_embd, n_head, n_batch, n_stream, n_kv, nem3, + nbq1, nbq2, nbq3, + nbk2, nbk3, + nbw1, nbw3, + nbm1, nbm3, + nb1, nb3, + k->type, + ctx.stream()); +} diff --git a/ggml/src/ggml-sycl/lightning-indexer.hpp b/ggml/src/ggml-sycl/lightning-indexer.hpp new file mode 100644 index 000000000..0b88c418e --- /dev/null +++ b/ggml/src/ggml-sycl/lightning-indexer.hpp @@ -0,0 +1,8 @@ +#ifndef GGML_SYCL_LIGHTNING_INDEXER_HPP +#define GGML_SYCL_LIGHTNING_INDEXER_HPP + +#include "common.hpp" + +void ggml_sycl_op_lightning_indexer(ggml_backend_sycl_context & ctx, ggml_tensor * dst); + +#endif // GGML_SYCL_LIGHTNING_INDEXER_HPP diff --git a/tools/mtmd/clip-impl.h b/tools/mtmd/clip-impl.h index 6a633a015..c167f9447 100644 --- a/tools/mtmd/clip-impl.h +++ b/tools/mtmd/clip-impl.h @@ -591,6 +591,8 @@ struct clip_image_u8 { } }; +struct mtmd_serialization; // forward declaration + // For images, buf.size() == nx*ny*3 // Memory layout: RGBRGBRGB... // For seq, buf.size() == nx*ny*3*nt @@ -671,6 +673,9 @@ struct clip_image_f32 { return buf.empty(); } + void serialize(struct mtmd_serialization & ser) const; + void deserialize(struct mtmd_serialization & ser); + private: std::vector buf; int nx_ = 0; @@ -754,6 +759,9 @@ struct clip_image_f32_batch { } return new_batch; } + + void serialize(struct mtmd_serialization & ser) const; + void deserialize(struct mtmd_serialization & ser); }; // diff --git a/tools/mtmd/clip-model.h b/tools/mtmd/clip-model.h index 101f49cd1..7db01b576 100644 --- a/tools/mtmd/clip-model.h +++ b/tools/mtmd/clip-model.h @@ -170,6 +170,17 @@ struct clip_hparams { warmup_image_size = static_cast(std::sqrt(image_max_pixels)); } + // used by longest_edge preprocessor (no model-specific value for min/max tokens) + void set_limit_image_tokens() { + const int patch_area = patch_size * patch_size * n_merge * n_merge; + if (custom_image_min_tokens > 0) { + image_min_pixels = custom_image_min_tokens * patch_area; + } + if (custom_image_max_tokens > 0) { + image_max_pixels = custom_image_max_tokens * patch_area; + } + } + void set_warmup_n_tokens(int n_tokens) { int n_tok_per_side = static_cast(std::sqrt(n_tokens)); GGML_ASSERT(n_tok_per_side * n_tok_per_side == n_tokens && "n_tokens must be n*n"); diff --git a/tools/mtmd/clip.cpp b/tools/mtmd/clip.cpp index 3eea0047a..200fd23da 100644 --- a/tools/mtmd/clip.cpp +++ b/tools/mtmd/clip.cpp @@ -1510,6 +1510,7 @@ struct clip_model_loader { // use default llava-uhd preprocessing params get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false); get_u32(KEY_PREPROC_IMAGE_SIZE, hparams.image_longest_edge, false); + hparams.set_limit_image_tokens(); } break; case PROJECTOR_TYPE_LFM2: { @@ -1547,6 +1548,7 @@ struct clip_model_loader { get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false); hparams.image_longest_edge = hparams.image_size; get_u32(KEY_PREPROC_IMAGE_SIZE, hparams.image_longest_edge, false); + hparams.set_limit_image_tokens(); hparams.set_warmup_n_tokens(256); // avoid OOM on warmup } break; case PROJECTOR_TYPE_DOTS_OCR: @@ -1676,6 +1678,7 @@ struct clip_model_loader { if (hparams.image_longest_edge == 0) { hparams.image_longest_edge = 3024; } + // note: the step3vl preprocessor slices based on a fixed window grid, so it does not support custom min/max image tokens hparams.warmup_image_size = hparams.image_size; } break; case PROJECTOR_TYPE_YOUTUVL: diff --git a/tools/mtmd/mtmd-helper-gen.cpp b/tools/mtmd/mtmd-helper-gen.cpp index b52dc8e5a..85671d1a3 100644 --- a/tools/mtmd/mtmd-helper-gen.cpp +++ b/tools/mtmd/mtmd-helper-gen.cpp @@ -112,7 +112,6 @@ public: c2w_state.clear(); audio_pcm.clear(); overlay.clear(); - overlay_idx = 0; h_state_buf.clear(); out_buf.clear(); prompt_embd_buf.clear(); @@ -205,11 +204,9 @@ public: top_p = inp->top_p > 0 ? inp->top_p : 1.0f; out_type = inp->out_type; - // the text stream keeps flowing during generation: after frame k, the input adds - // trailing text row k on top of the codes embedding, then tts_eos, then tts_pad - for (int i = 3; i < n_ids - 5; i++) overlay.push_back(row(ids[(size_t) i])); - overlay.push_back(row(tts_eos)); - overlay.push_back(row(tts_pad)); + // the prompt above holds the whole text stream up to tts_eos, so every generated + // frame adds tts_pad on top of the codes embedding + overlay = row(tts_pad); return 0; } @@ -265,9 +262,7 @@ public: } std::vector fb(out.embd, out.embd + n_embd); - const auto & ov = overlay[std::min(overlay_idx, overlay.size() - 1)]; - for (int i = 0; i < n_embd; i++) fb[(size_t) i] += ov[(size_t) i]; - overlay_idx++; + for (int i = 0; i < n_embd; i++) fb[(size_t) i] += overlay[(size_t) i]; const int n_pos_per_embd = mrope ? 4 : 1; decode_embd_batch batch_embd(fb.data(), 1, n_pos_per_embd, n_embd); @@ -437,8 +432,7 @@ private: std::vector codes_buf; std::vector c2w_state; std::vector audio_pcm; - std::vector> overlay; - size_t overlay_idx = 0; + std::vector overlay; std::vector h_state_buf; mtmd_helper_gen_audio_outtype out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV; std::vector out_buf; diff --git a/tools/mtmd/mtmd-image.cpp b/tools/mtmd/mtmd-image.cpp index 10cfe52f5..073d83d45 100644 --- a/tools/mtmd/mtmd-image.cpp +++ b/tools/mtmd/mtmd-image.cpp @@ -139,50 +139,46 @@ struct img_tool { } } - // calculate the size of the **resized** image, while preserving the aspect ratio - // the calculated size will be aligned to the nearest multiple of align_size - // if H or W size is larger than longest_edge, it will be resized to longest_edge - static clip_image_size calc_size_preserved_ratio(const clip_image_size & inp_size, const int align_size, const int longest_edge) { - GGML_ASSERT(align_size > 0); - if (inp_size.width <= 0 || inp_size.height <= 0 || longest_edge <= 0) { + struct calc_size_opt { + int align_size = 1; + int min_pixels = 0; // 0 = disabled + int max_pixels = 0; // 0 = disabled + // applied before min/max_pixels, so min_pixels can push an edge back above longest_edge + int longest_edge = 0; // 0 = disabled + }; + + // calculate the size of the **resized** image, while preserving the aspect ratio and + // aligning to the nearest multiple of align_size ("smart_resize" in transformers code) + static clip_image_size calc_size_preserved_ratio(const clip_image_size & inp_size, const calc_size_opt & opts) { + GGML_ASSERT(opts.align_size > 0); + const int width = inp_size.width; + const int height = inp_size.height; + if (width <= 0 || height <= 0) { return {0, 0}; } - float scale = std::min(static_cast(longest_edge) / inp_size.width, - static_cast(longest_edge) / inp_size.height); + auto round_by_factor = [f = opts.align_size](float x) { return static_cast(std::round(x / static_cast(f))) * f; }; + auto ceil_by_factor = [f = opts.align_size](float x) { return static_cast(std::ceil(x / static_cast(f))) * f; }; + auto floor_by_factor = [f = opts.align_size](float x) { return static_cast(std::floor(x / static_cast(f))) * f; }; - float target_width_f = static_cast(inp_size.width) * scale; - float target_height_f = static_cast(inp_size.height) * scale; + int w_bar, h_bar; + if (opts.longest_edge > 0) { + const float scale = std::min(static_cast(opts.longest_edge) / width, + static_cast(opts.longest_edge) / height); + w_bar = ceil_by_factor(width * scale); + h_bar = ceil_by_factor(height * scale); + } else { + // always align up first + w_bar = std::max(opts.align_size, round_by_factor(width)); + h_bar = std::max(opts.align_size, round_by_factor(height)); + } - auto ceil_by_factor = [f = align_size](float x) { return static_cast(std::ceil(x / static_cast(f))) * f; }; - int aligned_width = ceil_by_factor(target_width_f); - int aligned_height = ceil_by_factor(target_height_f); - - return {aligned_width, aligned_height}; - } - - // calculate the size of the **resized** image, while preserving the aspect ratio - // the calculated size will have min_pixels <= W*H <= max_pixels - // this is referred as "smart_resize" in transformers code - static clip_image_size calc_size_preserved_ratio(const clip_image_size & inp_size, const int align_size, const int min_pixels, const int max_pixels) { - GGML_ASSERT(align_size > 0); - const int width = inp_size.width; - const int height = inp_size.height; - - auto round_by_factor = [f = align_size](float x) { return static_cast(std::round(x / static_cast(f))) * f; }; - auto ceil_by_factor = [f = align_size](float x) { return static_cast(std::ceil(x / static_cast(f))) * f; }; - auto floor_by_factor = [f = align_size](float x) { return static_cast(std::floor(x / static_cast(f))) * f; }; - - // always align up first - int h_bar = std::max(align_size, round_by_factor(height)); - int w_bar = std::max(align_size, round_by_factor(width)); - - if (h_bar * w_bar > max_pixels) { - const auto beta = std::sqrt(static_cast(height * width) / max_pixels); - h_bar = std::max(align_size, floor_by_factor(height / beta)); - w_bar = std::max(align_size, floor_by_factor(width / beta)); - } else if (h_bar * w_bar < min_pixels) { - const auto beta = std::sqrt(static_cast(min_pixels) / (height * width)); + if (opts.max_pixels > 0 && h_bar * w_bar > opts.max_pixels) { + const auto beta = std::sqrt(static_cast(height) * width / opts.max_pixels); + h_bar = std::max(opts.align_size, floor_by_factor(height / beta)); + w_bar = std::max(opts.align_size, floor_by_factor(width / beta)); + } else if (opts.min_pixels > 0 && h_bar * w_bar < opts.min_pixels) { + const auto beta = std::sqrt(static_cast(opts.min_pixels) / (static_cast(height) * width)); h_bar = ceil_by_factor(height * beta); w_bar = ceil_by_factor(width * beta); } @@ -937,9 +933,12 @@ mtmd_image_preproc_out mtmd_image_preprocessor_dyn_size::preprocess(const clip_i const int cur_merge = hparams.n_merge; const clip_image_size target_size = img_tool::calc_size_preserved_ratio( original_size, - hparams.patch_size * cur_merge, - hparams.image_min_pixels, - hparams.image_max_pixels); + { + /* align_size */ hparams.patch_size * cur_merge, + /* min_pixels */ hparams.image_min_pixels, + /* max_pixels */ hparams.image_max_pixels, + /* longest_edge */ 0, + }); img_tool::resize(img, resized_image, target_size, hparams.image_resize_algo, hparams.image_resize_pad, @@ -961,8 +960,12 @@ mtmd_image_preproc_out mtmd_image_preprocessor_longest_edge::preprocess(const cl const int cur_merge = hparams.n_merge == 0 ? 1 : hparams.n_merge; const clip_image_size target_size = img_tool::calc_size_preserved_ratio( original_size, - hparams.patch_size * cur_merge, - hparams.image_longest_edge); + { + /* align_size */ hparams.patch_size * cur_merge, + /* min_pixels */ std::max(0, hparams.image_min_pixels), + /* max_pixels */ std::max(0, hparams.image_max_pixels), + /* longest_edge */ hparams.image_longest_edge, + }); img_tool::resize(img, resized_image, target_size, hparams.image_resize_algo, hparams.image_resize_pad, @@ -1000,8 +1003,8 @@ mtmd_image_preprocessor_llava_uhd::slice_instructions mtmd_image_preprocessor_lf mtmd_image_preprocessor_llava_uhd::slice_instructions inst; const int align_size = hparams.patch_size * hparams.n_merge; inst.overview_size = img_tool::calc_size_preserved_ratio( - original_size, align_size, - hparams.image_min_pixels, hparams.image_max_pixels); + original_size, + { align_size, hparams.image_min_pixels, hparams.image_max_pixels, 0 }); // tile if either dimension exceeds tile_size with tolerance const bool needs_tiling = original_size.width > tile_size * max_pixels_tolerance || original_size.height > tile_size * max_pixels_tolerance; @@ -1109,7 +1112,8 @@ mtmd_image_preproc_out mtmd_image_preprocessor_idefics3::preprocess(const clip_i // CITE: https://github.com/huggingface/transformers/blob/main/src/transformers/models/idefics3/image_processing_idefics3.py#L737 const clip_image_size original_size = img.get_size(); const clip_image_size refined_size = img_tool::calc_size_preserved_ratio( - original_size, hparams.image_size, hparams.image_longest_edge); + original_size, + { hparams.image_size, std::max(0, hparams.image_min_pixels), std::max(0, hparams.image_max_pixels), hparams.image_longest_edge }); // LOG_INF("%s: original size: %d x %d, refined size: %d x %d\n", // __func__, original_size.width, original_size.height, // refined_size.width, refined_size.height); diff --git a/tools/mtmd/mtmd.cpp b/tools/mtmd/mtmd.cpp index ff90d6818..6ef4a9d3a 100644 --- a/tools/mtmd/mtmd.cpp +++ b/tools/mtmd/mtmd.cpp @@ -22,8 +22,123 @@ #include #include #include +#include #include +// remember to bump this if the serialization format changes +#define MTMD_SERIALIZATION_VERSION 1 + +struct mtmd_serialization { + // note: using 64-bit here for future-proofing + uint64_t version = MTMD_SERIALIZATION_VERSION; + std::vector data; + size_t read_pos = 0; // cursor used when reading + + // for writing + mtmd_serialization(uint64_t version) : version(version) { + write(version); + } + + // for reading + mtmd_serialization(uint64_t version, const char * buf, size_t len) { + // copy buf to data + data.assign(buf, buf + len); + uint64_t ver_in = read(); + if (ver_in != version) { + throw std::runtime_error("version mismatch"); + } + this->version = ver_in; + } + + template + void write(T value) { + static_assert(std::is_trivially_copyable::value && !std::is_same::value, + "T must be trivially copyable and not bool"); + const char * p = reinterpret_cast(&value); + data.insert(data.end(), p, p + sizeof(T)); + } + + template + T read() { + static_assert(std::is_trivially_copyable::value && !std::is_same::value, + "T must be trivially copyable and not bool"); + if (read_pos + sizeof(T) > data.size()) { + throw std::runtime_error("read OOB"); + } + T value; + std::memcpy(&value, data.data() + read_pos, sizeof(T)); + read_pos += sizeof(T); + return value; + } + +}; + +template <> +void mtmd_serialization::write(bool value) { + write(value ? 1 : 0); +} +template <> +bool mtmd_serialization::read() { + return read() != 0; +} + +template <> +void mtmd_serialization::write(std::string value) { + write(value.size()); + data.insert(data.end(), value.begin(), value.end()); +} +template <> +std::string mtmd_serialization::read() { + uint64_t len = read(); + if (read_pos + len > data.size()) { + throw std::runtime_error("read_string OOB"); + } + std::string str(data.data() + read_pos, len); + read_pos += len; + return str; +} + +// only mtmd.cpp needs these, so they're implemented here rather than in clip-impl.h +void clip_image_f32::serialize(mtmd_serialization & ser) const { + // remember to bump MTMD_SERIALIZATION_VERSION if this is changed + // note: buf is intentionally NOT serialized; the loaded clip_image_f32 will always be a placeholder + ser.write(add_viewsep); + ser.write(add_newline); + ser.write((int32_t)nx_); + ser.write((int32_t)ny_); +} +void clip_image_f32::deserialize(mtmd_serialization & ser) { + add_viewsep = ser.read(); + add_newline = ser.read(); + nx_ = ser.read(); + ny_ = ser.read(); + buf.clear(); // always a placeholder after loading +} + +void clip_image_f32_batch::serialize(mtmd_serialization & ser) const { + // remember to bump MTMD_SERIALIZATION_VERSION if this is changed + ser.write(is_audio); + ser.write(entries.size()); + for (const auto & entry : entries) { + entry.serialize(ser); + } +} +void clip_image_f32_batch::deserialize(mtmd_serialization & ser) { + is_audio = ser.read(); + uint64_t n = ser.read(); + constexpr size_t min_entry_bytes = sizeof(uint8_t) * 2 + sizeof(int32_t) * 2; + if (n > (ser.data.size() - ser.read_pos) / min_entry_bytes) { + throw std::runtime_error("entries count exceeds buffer size"); + } + entries.clear(); + entries.reserve(n); + for (uint64_t i = 0; i < n; i++) { + clip_image_f32 entry; + entry.deserialize(ser); + entries.push_back(std::move(entry)); + } +} + // for still image data, layout is RGBRGBRGB... // length of data must be nx * ny * 3 bytes // @@ -83,6 +198,7 @@ enum mtmd_pos_type { MTMD_POS_TYPE_NORMAL, // number of positions equals to number of tokens MTMD_POS_TYPE_MROPE, // qwen-vl mrope style, each image takes max(t,h,w) position indexes MTMD_POS_TYPE_HUNYUANVL, // HunyuanVL mrope + BOI/EOI/newline layout with XD-RoPE dim-3 + MTMD_POS_TYPE_COUNT, // for validation }; struct mtmd_image_tokens { @@ -136,6 +252,30 @@ struct mtmd_image_tokens { id }; } + + void serialize(mtmd_serialization & ser) const { + // remember to bump MTMD_SERIALIZATION_VERSION if this is changed + ser.write(nx); + ser.write(ny); + ser.write((uint32_t)pos); + ser.write(image_idx); + ser.write(n_temporal_merge); + ser.write(id); + batch_f32.serialize(ser); + } + void deserialize(mtmd_serialization & ser) { + nx = ser.read(); + ny = ser.read(); + uint32_t pos_raw = ser.read(); + if (pos_raw >= MTMD_POS_TYPE_COUNT) { + throw std::runtime_error("invalid pos type"); + } + pos = (mtmd_pos_type)pos_raw; + image_idx = ser.read(); + n_temporal_merge = ser.read(); + id = ser.read(); + batch_f32.deserialize(ser); + } }; using mtmd_image_tokens_ptr = std::unique_ptr; @@ -161,6 +301,18 @@ struct mtmd_audio_tokens { id }; } + + void serialize(mtmd_serialization & ser) const { + // remember to bump MTMD_SERIALIZATION_VERSION if this is changed + ser.write(n_tokens); + ser.write(id); + batch_f32.serialize(ser); + } + void deserialize(mtmd_serialization & ser) { + n_tokens = ser.read(); + id = ser.read(); + batch_f32.deserialize(ser); + } }; using mtmd_audio_tokens_ptr = std::unique_ptr; @@ -192,6 +344,66 @@ struct mtmd_input_chunk { } return false; } + + void serialize(mtmd_serialization & ser) const { + // remember to bump MTMD_SERIALIZATION_VERSION if this is changed + ser.write((uint32_t)type); + + ser.write(tokens_text.size()); + for (llama_token tok : tokens_text) { + ser.write((int32_t)tok); + } + + ser.write(tokens_image != nullptr); + if (tokens_image) { + tokens_image->serialize(ser); + } + + ser.write(tokens_audio != nullptr); + if (tokens_audio) { + tokens_audio->serialize(ser); + } + } + void deserialize(mtmd_serialization & ser) { + uint32_t type_raw = ser.read(); + if (type_raw >= MTMD_INPUT_CHUNK_TYPE_COUNT) { + throw std::runtime_error("invalid chunk type"); + } + type = (mtmd_input_chunk_type)type_raw; + + uint64_t n_tokens_text = ser.read(); + // reject before resize() so a tiny corrupted/malicious buffer can't force a huge allocation + if (n_tokens_text > (ser.data.size() - ser.read_pos) / sizeof(int32_t)) { + throw std::runtime_error("tokens_text length exceeds buffer size"); + } + tokens_text.resize(n_tokens_text); + for (uint64_t i = 0; i < n_tokens_text; i++) { + tokens_text[i] = (llama_token)ser.read(); + } + + if (ser.read()) { + tokens_image = std::make_unique(); + tokens_image->deserialize(ser); + } else { + tokens_image.reset(); + } + + if (ser.read()) { + tokens_audio = std::make_unique(); + tokens_audio->deserialize(ser); + } else { + tokens_audio.reset(); + } + + // catch buffers where the declared type doesn't match which payload is actually present, + // so a mismatched chunk can't slip through and null-deref/abort later in an accessor + if (type == MTMD_INPUT_CHUNK_TYPE_IMAGE && !tokens_image) { + throw std::runtime_error("type is IMAGE but tokens_image is missing"); + } + if (type == MTMD_INPUT_CHUNK_TYPE_AUDIO && !tokens_audio) { + throw std::runtime_error("type is AUDIO but tokens_audio is missing"); + } + } }; struct mtmd_input_chunks { @@ -2043,6 +2255,42 @@ void mtmd_input_chunk_free(mtmd_input_chunk * chunk) { } } +int32_t mtmd_input_chunk_save(const mtmd_input_chunk * chunk, char * out_buf, size_t out_len, size_t * expected_out_len) { + try { + mtmd_serialization ser(MTMD_SERIALIZATION_VERSION); + chunk->serialize(ser); + + if (expected_out_len) { + *expected_out_len = ser.data.size(); + } + if (!out_buf) { + // caller is only querying the required size + return 0; + } + if (out_len < ser.data.size()) { + LOG_ERR("%s: out_buf is too small, need %zu bytes, got %zu\n", __func__, ser.data.size(), out_len); + return -1; + } + std::memcpy(out_buf, ser.data.data(), ser.data.size()); + return 0; + } catch (const std::exception & e) { + LOG_ERR("%s: %s\n", __func__, e.what()); + return -1; + } +} + +mtmd_input_chunk * mtmd_input_chunk_load(const char * buf, size_t len) { + try { + mtmd_serialization ser(MTMD_SERIALIZATION_VERSION, buf, len); + mtmd::input_chunk_ptr chunk(new mtmd_input_chunk()); + chunk->deserialize(ser); + return chunk.release(); + } catch (const std::exception & e) { + LOG_ERR("%s: %s\n", __func__, e.what()); + return nullptr; + } +} + // mtmd_image_tokens size_t mtmd_image_tokens_get_n_tokens(const mtmd_image_tokens * image_tokens) { diff --git a/tools/mtmd/mtmd.h b/tools/mtmd/mtmd.h index 84651f8dc..e5063d114 100644 --- a/tools/mtmd/mtmd.h +++ b/tools/mtmd/mtmd.h @@ -55,6 +55,7 @@ enum mtmd_input_chunk_type { MTMD_INPUT_CHUNK_TYPE_TEXT, MTMD_INPUT_CHUNK_TYPE_IMAGE, MTMD_INPUT_CHUNK_TYPE_AUDIO, + MTMD_INPUT_CHUNK_TYPE_COUNT, // for validation }; // opaque types @@ -232,6 +233,15 @@ MTMD_API llama_pos mtmd_input_chunk_get_n_pos (const mtmd MTMD_API mtmd_input_chunk * mtmd_input_chunk_copy(const mtmd_input_chunk * chunk); MTMD_API void mtmd_input_chunk_free(mtmd_input_chunk * chunk); +// save/load an input chunk to/from a buffer (useful for KV save/load) +// important: only chunk's metadata will be saved, the actual image/audio data will not be saved +// the loaded chunk will always be a placeholder, cannot be used for mtmd_encode() or mtmd_batch_encode() +// out_buf can be nullptr (to query expected_out_len) +// returns 0 on success, non-zero on failure +MTMD_API int32_t mtmd_input_chunk_save(const mtmd_input_chunk * chunk, char * out_buf, size_t out_len, size_t * expected_out_len); +// returns nullptr on failure +MTMD_API mtmd_input_chunk * mtmd_input_chunk_load(const char * buf, size_t len); + // mtmd_image_tokens // diff --git a/tools/server/server-models.cpp b/tools/server/server-models.cpp index 2fd9519c1..93e940951 100644 --- a/tools/server/server-models.cpp +++ b/tools/server/server-models.cpp @@ -70,6 +70,188 @@ struct server_subproc { } }; +struct server_lru_sched { + server_lru_sched(server_models & models) : models(models) {} + + bool has_capacity(std::unique_lock & lk) { + check_lock(lk); + return models.base_params.models_max <= 0 + || count_running() < (size_t) models.base_params.models_max; + } + + // returns "" if no model can be given up + std::string pick_victim(std::unique_lock & lk, const std::string & exclude) { + check_lock(lk); + std::string victim; + int64_t victim_last_used = 0; + for (const auto & m : models.mapping) { + if (m.first == exclude) { + continue; + } + // a busy model is mid-request, one still coming up has no request to finish + if (m.second.req_count != 0 || !m.second.meta.is_ready_or_sleep()) { + continue; + } + if (victim.empty() || m.second.meta.last_used < victim_last_used) { + victim = m.first; + victim_last_used = m.second.meta.last_used; + } + } + return victim; + } + + // requests wanting the same model share one entry, so they all need only one slot + // and all get unblocked by the single load that entry performs + void join(std::unique_lock & lk, const std::string & model_id) { + check_lock(lk); + if (entry_t * e = find(model_id)) { + e->n_waiters++; + SRV_INF("request for name=%s joined the queue, %d waiting\n", model_id.c_str(), e->n_waiters); + return; + } + queue.push_back({ model_id, 1, false, false }); + SRV_INF("models_max reached, request for name=%s queued at position %zu\n", + model_id.c_str(), queue.size()); + } + + void leave(std::unique_lock & lk, const std::string & model_id) { + check_lock(lk); + for (auto it = queue.begin(); it != queue.end(); ++it) { + if (it->model_id == model_id) { + if (--it->n_waiters <= 0) { + queue.erase(it); // last one waiting for this model went away + } + return; + } + } + } + + bool queue_empty(std::unique_lock & lk) { + check_lock(lk); + return queue.empty(); + } + + // true if it is this model's turn to load, and nobody is loading it yet + bool try_claim(std::unique_lock & lk, const std::string & model_id) { + check_lock(lk); + if (queue.empty() || queue.front().model_id != model_id || queue.front().loading) { + return false; + } + if (!has_capacity(lk)) { + return false; + } + queue.front().loading = true; + return true; + } + + // ok means the model is up: drop the entry, the other waiters just watch its status now + void claim_done(std::unique_lock & lk, const std::string & model_id, bool ok) { + check_lock(lk); + for (auto it = queue.begin(); it != queue.end(); ++it) { + if (it->model_id == model_id) { + if (ok) { + queue.erase(it); + } else { + it->loading = false; + } + return; + } + } + } + + // a model is on its way out for this entry, so other requests do not also give up one + void mark_slot_pending(std::unique_lock & lk, const std::string & model_id) { + check_lock(lk); + if (entry_t * e = find(model_id)) { + e->slot_pending = true; + } + } + + // model_id went idle: give up its slot if a queued request needs one + // thread-safe, caller must NOT hold models.mutex + void on_model_idle(const std::string & model_id) { + if (models.base_params.models_max <= 0) { + return; // no limit, nothing is ever queued + } + { + std::unique_lock lk(models.mutex); + if (queue.empty()) { + return; + } + size_t promised = 0; + bool has_unserved = false; + for (const auto & e : queue) { + if (e.needs_slot()) { + has_unserved = true; + } else { + promised++; + } + } + if (!has_unserved) { + return; + } + if ((int) count_running() - (int) promised < models.base_params.models_max) { + return; // a slot is already on its way + } + // never give up a model that a queued request wants + for (const auto & e : queue) { + if (e.model_id == model_id) { + return; + } + } + auto it = models.mapping.find(model_id); + if (it == models.mapping.end() || it->second.req_count != 0 || !it->second.meta.is_ready_or_sleep()) { + return; + } + for (auto & e : queue) { + if (!e.slot_pending) { + e.slot_pending = true; + break; + } + } + } + SRV_INF("model name=%s went idle, giving up its slot to a queued request\n", model_id.c_str()); + models.unload(model_id); + } + + private: + struct entry_t { + std::string model_id; + int n_waiters; // requests waiting for this model + bool slot_pending; // a model is already being evicted for this entry + bool loading; // one of the waiters is doing the load right now + + // a slot is already coming, or already taken by the load in flight + bool needs_slot() const { return !slot_pending && !loading; } + }; + + entry_t * find(const std::string & model_id) { + for (auto & e : queue) { + if (e.model_id == model_id) { + return &e; + } + } + return nullptr; + } + + void check_lock(std::unique_lock & lk) { + GGML_ASSERT(lk.owns_lock() && lk.mutex() == &models.mutex); + } + + size_t count_running() { + size_t count = 0; + for (const auto & m : models.mapping) { + if (m.second.meta.is_running()) { + count++; + } + } + return count; + } + + server_models & models; + std::deque queue; +}; + // short loopback budget for the resumable stream router to child JSON calls (probe, lookup, // delete). distinct from params.timeout_read/write which only applies to the generation proxy static constexpr int STREAM_LOOKUP_TIMEOUT_MS = 250; @@ -229,7 +411,8 @@ server_models::server_models( : ctx_preset(LLAMA_EXAMPLE_SERVER), base_params(params), base_env(get_environment()), - base_preset(ctx_preset.load_from_args(argc, argv)) { + base_preset(ctx_preset.load_from_args(argc, argv)), + sched(std::make_unique(*this)) { // clean up base preset unset_reserved_args(base_preset, true); // set binary path @@ -241,8 +424,11 @@ server_models::server_models( LOG_WRN("using original argv[0] as fallback: %s\n", argv[0]); } load_models(); + debug_fake_timing = !common_get_env("LLAMA_SERVER_DEBUG_FAKE_TIMING").empty(); } +server_models::~server_models() = default; + void server_models::add_model(server_model_meta && meta) { if (mapping.find(meta.name) != mapping.end()) { throw std::runtime_error(string_format("model '%s' appears multiple times", meta.name.c_str())); @@ -713,22 +899,15 @@ void server_models::unload_lru() { return; // no limit } // remove one of the servers if we passed the models_max (least recently used - LRU) - std::string lru_model_name = ""; - int64_t lru_last_used = ggml_time_ms(); - size_t count_active = 0; + std::string lru_model_name; { std::unique_lock lk(mutex); - for (const auto & m : mapping) { - if (m.second.meta.is_running()) { - count_active++; - if (m.second.meta.last_used < lru_last_used) { - lru_model_name = m.first; - lru_last_used = m.second.meta.last_used; - } - } + if (sched->has_capacity(lk)) { + return; } + lru_model_name = sched->pick_victim(lk, ""); } - if (!lru_model_name.empty() && count_active >= (size_t)base_params.models_max) { + if (!lru_model_name.empty()) { SRV_INF("models_max limit reached, removing LRU name=%s\n", lru_model_name.c_str()); unload(lru_model_name); // wait for unload to complete @@ -746,6 +925,11 @@ void server_models::load(const std::string & name) { } void server_models::load(const std::string & name, const load_options & opts) { + if (debug_fake_timing) { + // do not hold the mutex here, other requests must keep making progress + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + if (!opts.custom_meta.has_value()) { if (!has_model(name)) { throw std::runtime_error("model name=" + name + " is not found"); @@ -1138,7 +1322,7 @@ void server_models::wait(std::unique_lock & lk, const std::string & }); } -bool server_models::ensure_model_ready(const std::string & name) { +bool server_models::ensure_model_ready(const std::string & name, const std::function & should_stop) { auto meta = get_meta(name); if (!meta.has_value()) { throw std::runtime_error("model name=" + name + " is not found"); @@ -1149,25 +1333,112 @@ bool server_models::ensure_model_ready(const std::string & name) { if (meta->status == SERVER_MODEL_STATUS_SLEEPING) { return false; // child is sleeping but still running; new request will wake it up } - if (meta->status == SERVER_MODEL_STATUS_UNLOADED) { - SRV_INF("model name=%s is not loaded, loading...\n", name.c_str()); - load(name); - } - // wait for loading to complete - SRV_INF("waiting until model name=%s is fully loaded...\n", name.c_str()); - wait(name, [&meta](const server_model_meta & new_meta) { - if (new_meta.status != SERVER_MODEL_STATUS_LOADING) { - meta = new_meta; // update meta for final check after wait - return true; + bool queued = false; + bool did_load = false; + std::string victim; + { + std::unique_lock lk(mutex); + auto it = mapping.find(name); + if (it != mapping.end() && it->second.meta.status == SERVER_MODEL_STATUS_UNLOADED) { + bool has_capacity = sched->has_capacity(lk); + if (has_capacity && sched->queue_empty(lk)) { + lk.unlock(); + SRV_INF("model name=%s is not loaded, loading...\n", name.c_str()); + load(name); + did_load = true; + } else { + // also queue when a slot looks free but others wait already, else they starve + sched->join(lk, name); + queued = true; + if (!has_capacity) { + // an idle model may sit here right now, do not wait for a request to end + victim = sched->pick_victim(lk, name); + if (!victim.empty()) { + sched->mark_slot_pending(lk, name); + } + } + } } - return false; - }); - - // check final status - if (!meta.has_value() || meta->is_failed()) { - throw std::runtime_error("model name=" + name + " failed to load"); } + if (!victim.empty()) { + SRV_INF("evicting idle LRU name=%s to make room for name=%s\n", victim.c_str(), name.c_str()); + unload(victim); + } + + // while queued, this is also where the load happens: the head of the queue does it + SRV_INF("waiting until model name=%s is fully loaded...\n", name.c_str()); + std::unique_lock lk(mutex); + auto leave_queue = [this, &queued, &lk, &name]() { + if (queued) { + sched->leave(lk, name); + queued = false; + } + }; + + try { + bool saw_loading = false; + while (true) { + auto it = mapping.find(name); + if (it == mapping.end()) { + break; // removed by another code path, nothing to wait for + } + const server_model_status status = it->second.meta.status; + + if (status == SERVER_MODEL_STATUS_LOADED || status == SERVER_MODEL_STATUS_SLEEPING) { + break; + } + if (status == SERVER_MODEL_STATUS_DOWNLOADING || status == SERVER_MODEL_STATUS_DOWNLOADED) { + break; // do not wait on a download child + } + if (status == SERVER_MODEL_STATUS_LOADING) { + saw_loading = true; + } else if (status == SERVER_MODEL_STATUS_UNLOADED) { + if (did_load || saw_loading) { + // a spawn happened and the instance came back down + if (it->second.meta.is_failed()) { + throw std::runtime_error("model name=" + name + " failed to load"); + } + break; // unloaded by another code path, caller reports "not running" + } + if (!queued) { + break; // not queued, and the load someone else started fell over + } + } + + if (should_stop && should_stop()) { + // if a model was evicted for us, the free slot goes to the next waiter + throw std::runtime_error("request cancelled while waiting for model name=" + name); + } + + // our turn: our model is at the head, and a slot really did free up + if (status == SERVER_MODEL_STATUS_UNLOADED && sched->try_claim(lk, name)) { + lk.unlock(); + bool ok = true; + try { + SRV_INF("slot available, loading queued model name=%s\n", name.c_str()); + load(name); + did_load = true; + } catch (const std::exception & e) { + // lost a race for the slot, stay in line and retry + SRV_WRN("queued load of name=%s did not go through: %s\n", name.c_str(), e.what()); + ok = false; + } + lk.lock(); + sched->claim_done(lk, name, ok); + if (ok) { + queued = false; // entry is gone, the other waiters watch the status now + } + continue; + } + + cv.wait_for(lk, std::chrono::milliseconds(200)); + } + } catch (...) { + leave_queue(); + throw; + } + leave_queue(); return true; } @@ -1180,9 +1451,16 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co if (!meta->is_running()) { throw std::invalid_argument("model name=" + name + " is not running"); } - if (update_last_used) { + { std::unique_lock lk(mutex); - mapping[name].meta.last_used = ggml_time_ms(); + if (update_last_used) { + mapping[name].meta.last_used = ggml_time_ms(); + } + mapping[name].req_count++; + } + if (debug_fake_timing) { + // sleep after req_count++, so the model counts as busy while we wait here + std::this_thread::sleep_for(std::chrono::seconds(2)); } SRV_INF("proxying request to model %s on port %d\n", name.c_str(), meta->port); std::string proxy_path = req.path; @@ -1198,13 +1476,29 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co req.headers, req.body, req.files, - // a detached request belongs to a replay session that outlives the client socket: - // it reaches the child even when the downstream died during the load wait, the - // session buffer is the recipient and DELETE remains the stop - detached ? std::function([]() { return false; }) : req.should_stop, + // a detached request belongs to a replay session + detached + ? std::function([]() { return false; }) + : req.should_stop, base_params.timeout_read, base_params.timeout_write ); + + proxy->cleanup = [this, name]() { + bool went_idle = false; + { + std::unique_lock lk(mutex); + auto it = mapping.find(name); + if (it != mapping.end() && it->second.req_count > 0) { + it->second.req_count--; + went_idle = it->second.req_count == 0; + } + } + if (went_idle) { + sched->on_model_idle(name); + } + }; + return proxy; } @@ -1568,7 +1862,7 @@ void server_models_routes::init_routes() { return error_res; } if (autoload) { - models.ensure_model_ready(name); + models.ensure_model_ready(name, req.should_stop); } return models.proxy_request(req, method, name, false); }; @@ -1588,7 +1882,9 @@ void server_models_routes::init_routes() { // this request instead of leaving an orphan generation std::string conv_id = server_stream_conv_id_from_headers(req.headers); uint64_t ticket = models.conv_models.remember(conv_id, name); - bool waited = autoload && models.ensure_model_ready(name); + // a dead socket must not cancel a session request, only a stop does (checked right below) + auto should_stop = ticket == 0 ? req.should_stop : nullptr; + bool waited = autoload && models.ensure_model_ready(name, should_stop); if (ticket != 0 && !models.conv_models.alive(conv_id, ticket)) { SRV_INF("request for conv_id=%s cancelled while model name=%s was loading\n", conv_id.c_str(), name.c_str()); @@ -2064,7 +2360,7 @@ server_http_proxy::server_http_proxy( cli->set_write_timeout(timeout_read, 0); // reversed for cli (client) vs srv (server) cli->set_read_timeout(timeout_write, 0); this->status = 500; // to be overwritten upon response - this->cleanup = [pipe]() { + this->cleanup_pipes = [pipe]() { pipe->close_read(); pipe->close_write(); }; diff --git a/tools/server/server-models.h b/tools/server/server-models.h index 614798186..615acb577 100644 --- a/tools/server/server-models.h +++ b/tools/server/server-models.h @@ -84,7 +84,6 @@ struct server_model_meta { int exit_code = 0; // exit code of the model instance process (only valid if status == FAILED) int stop_timeout = 0; // seconds to wait before force-killing the model instance during shutdown mtmd_caps multimodal; // multimodal capabilities - // bool need_download = false; // whether the model needs to be downloaded before loading // TODO @ngxson: implement this bool is_ready() const { return status == SERVER_MODEL_STATUS_LOADED; @@ -94,6 +93,10 @@ struct server_model_meta { return status == SERVER_MODEL_STATUS_LOADED || status == SERVER_MODEL_STATUS_LOADING || status == SERVER_MODEL_STATUS_SLEEPING; } + bool is_ready_or_sleep() const { + return status == SERVER_MODEL_STATUS_LOADED || status == SERVER_MODEL_STATUS_SLEEPING; + } + bool is_failed() const { return status == SERVER_MODEL_STATUS_UNLOADED && exit_code != 0; } @@ -103,16 +106,19 @@ struct server_model_meta { }; struct server_models_routes; -struct server_subproc; // defined in server-models.cpp +struct server_subproc; // defined in server-models.cpp +struct server_lru_sched; // defined in server-models.cpp struct server_models { friend struct server_models_routes; + friend struct server_lru_sched; private: struct instance_t { std::shared_ptr subproc; // shared between main thread and monitoring thread std::thread th; server_model_meta meta; + int req_count = 0; // number of active proxy requests }; std::mutex mutex; @@ -191,6 +197,12 @@ private: std::vector base_env; common_preset base_preset; // base preset from llama-server CLI args + // queue of requests waiting for a models_max slot + std::unique_ptr sched; + + // if true, add some delay to simulate works (useful for testing) + bool debug_fake_timing = false; + void update_meta(const std::string & name, const server_model_meta & meta); // unload least recently used models if the limit is reached @@ -207,6 +219,7 @@ public: conv_model_tracker conv_models; server_models(const common_params & params, int argc, char ** argv); + ~server_models(); server_response sse; // for real-time updates via SSE endpoint @@ -263,7 +276,9 @@ public: // ensure the model is in ready state (thread-safe) // return false if model is ready // otherwise, load the model and blocking wait until it's ready, then return true (meta may need to be refreshed) - bool ensure_model_ready(const std::string & name); + // if models_max is reached, the request waits in a queue until a slot frees up + // throws if the load fails, or if should_stop fires while waiting + bool ensure_model_ready(const std::string & name, const std::function & should_stop = nullptr); // proxy an HTTP request to the model instance server_http_res_ptr proxy_request(const server_http_req & req, const std::string & method, const std::string & name, bool update_last_used, bool detached = false); @@ -343,7 +358,6 @@ struct server_models_routes { */ struct server_http_proxy : server_http_res { std::function cleanup = nullptr; -public: server_http_proxy(const std::string & method, const std::string & scheme, const std::string & host, @@ -357,11 +371,15 @@ public: int32_t timeout_write ); ~server_http_proxy() { + if (cleanup_pipes) { + cleanup_pipes(); + } if (cleanup) { cleanup(); } } private: + std::function cleanup_pipes = nullptr; std::thread thread; struct msg_t { std::map headers; diff --git a/tools/server/server-schema.cpp b/tools/server/server-schema.cpp index 5cef3908f..5d7fa6ae6 100644 --- a/tools/server/server-schema.cpp +++ b/tools/server/server-schema.cpp @@ -519,7 +519,7 @@ task_params eval_llama_cmpl_schema( const json & data) { task_params params; - // Sampling parameter defaults are loaded from the global server context (but individual requests can still them) + // Sampling parameter defaults are loaded from the global server context (but individual requests can still override them) params.sampling = params_base.sampling; params.speculative = params_base.speculative; params.n_keep = params_base.n_keep; diff --git a/tools/server/tests/conftest.py b/tools/server/tests/conftest.py index c7ed77596..5dfde4079 100644 --- a/tools/server/tests/conftest.py +++ b/tools/server/tests/conftest.py @@ -15,7 +15,7 @@ def stop_server_after_each_test(): server.stop() -@pytest.fixture(scope="module", autouse=True) -def do_something(): +@pytest.fixture(scope="session", autouse=True) +def load_server_presets(): # this will be run once per test session, before any tests ServerPreset.load_all() diff --git a/tools/server/tests/tests.sh b/tools/server/tests/tests.sh index 709b5841a..8d6681193 100755 --- a/tools/server/tests/tests.sh +++ b/tools/server/tests/tests.sh @@ -14,10 +14,10 @@ fi if [ $# -lt 1 ] then if [[ "${SLOW_TESTS:-0}" == 1 ]]; then - pytest -v -x + pytest --durations=30 -v -x else - pytest -v -x -m "not slow" + pytest --durations=30 -v -x -m "not slow" fi else - pytest "$@" + pytest --durations=30 "$@" fi diff --git a/tools/server/tests/unit/test_router.py b/tools/server/tests/unit/test_router.py index 94165e520..5ab62666c 100644 --- a/tools/server/tests/unit/test_router.py +++ b/tools/server/tests/unit/test_router.py @@ -85,7 +85,7 @@ def _wait_for_model_status(model_id: str, desired: set[str], timeout: int = 60) last_status = _get_model_status(model_id) if last_status in desired: return last_status - time.sleep(1) + time.sleep(0.01) raise AssertionError( f"Timed out waiting for {model_id} to reach {desired}, last status: {last_status}" ) @@ -145,6 +145,156 @@ def test_router_models_max_evicts_lru(): assert _get_model_status(first) == "unloaded" +# server_lru_sched tests (relying on LLAMA_SERVER_DEBUG_FAKE_TIMING) + +MODEL_A = "ggml-org/tinygemma3-GGUF:Q8_0" +MODEL_B = "ggml-org/test-model-stories260K:F32" +MODEL_C = "ggml-org/test-model-stories260K-infill:F32" + + +def _tokenize(model_id: str, timeout: float | None = DEFAULT_REQUEST_TIMEOUT) -> ServerResponse: + return server.make_request( + "POST", "/tokenize", data={"model": model_id, "content": "hello world"}, timeout=timeout + ) + + +class _Bg: + """runs one request in a thread, keeps its result, error and finish time""" + + def __init__(self, fn): + self.result = None + self.error: Exception | None = None + self.done_at: float = 0.0 + self._thread = threading.Thread(target=self._run, args=(fn,), daemon=True) + + def _run(self, fn): + try: + self.result = fn() + except Exception as e: + self.error = e + self.done_at = time.time() + + def start(self): + self._thread.start() + return self + + def join(self, timeout: int = 180): + self._thread.join(timeout) + assert not self._thread.is_alive(), "background request did not finish in time" + return self + + def assert_ok(self, what: str): + assert self.error is None, f"{what} raised {self.error!r}" + assert self.result is not None and self.result.status_code == 200, \ + f"{what} failed: {self.result.status_code if self.result else None} {self.result.body if self.result else None}" + + +def test_router_queue_does_not_evict_busy_model(): + """a request that finds no free slot waits, and the model serving a request survives it""" + global server + server.models_max = 1 + server.start() + + _load_model_and_wait(MODEL_A, timeout=120) + + busy = _Bg(lambda: _tokenize(MODEL_A)).start() + time.sleep(0.5) # let the request reach the child and take the only slot + + # no slot free and MODEL_A is busy, so this queues instead of evicting mid-request + queued = _Bg(lambda: _tokenize(MODEL_B)).start() + + busy.join() + queued.join() + + # had MODEL_A been evicted while serving, its own request would have died + busy.assert_ok("request against the busy model") + queued.assert_ok("queued request") + + _wait_for_model_status(MODEL_B, {"loaded"}, timeout=120) + assert _get_model_status(MODEL_A) == "unloaded" + + +def test_router_queue_coalesces_requests_for_same_model(): + """many requests for one missing model share a slot, so only one model is given up""" + global server + server.models_max = 2 + server.start() + + _load_model_and_wait(MODEL_A, timeout=120) + _load_model_and_wait(MODEL_B, timeout=120) + + # keep MODEL_A busy so MODEL_B is the only model that can be given up + busy = _Bg(lambda: _tokenize(MODEL_A)).start() + time.sleep(0.5) + + waiters = [_Bg(lambda: _tokenize(MODEL_C)).start() for _ in range(3)] + + busy.join() + for w in waiters: + w.join() + + busy.assert_ok("request against the busy model") + for i, w in enumerate(waiters): + w.assert_ok(f"queued request {i}") + + _wait_for_model_status(MODEL_C, {"loaded"}, timeout=120) + # one entry for 3 requests means one eviction: MODEL_B goes, MODEL_A is left alone. + # without coalescing the leftover entries still ask for a slot, + # and MODEL_A is taken too as soon as it goes idle + assert _get_model_status(MODEL_A) == "loaded" + assert _get_model_status(MODEL_B) == "unloaded" + + +def test_router_queue_client_disconnect_keeps_model(): + """a client that leaves while queued must not cost a running model its slot""" + global server + server.models_max = 1 + server.start() + + _load_model_and_wait(MODEL_A, timeout=120) + + busy = _Bg(lambda: _tokenize(MODEL_A)).start() + time.sleep(0.5) + + # queues behind MODEL_A, then gives up long before MODEL_A goes idle + with pytest.raises(requests.exceptions.RequestException): + _tokenize(MODEL_B, timeout=1) + + busy.join() + busy.assert_ok("request against the busy model") + + # nobody is waiting anymore, so MODEL_A keeps its slot + time.sleep(3) + assert _get_model_status(MODEL_A) == "loaded" + assert _get_model_status(MODEL_B) == "unloaded" + + +def test_router_queue_is_fifo(): + """the queue is served in arrival order""" + global server + server.models_max = 1 + server.start() + + _load_model_and_wait(MODEL_A, timeout=120) + + busy = _Bg(lambda: _tokenize(MODEL_A)).start() + time.sleep(0.5) + + first = _Bg(lambda: _tokenize(MODEL_B)).start() + time.sleep(1) # keep the arrival order unambiguous + second = _Bg(lambda: _tokenize(MODEL_C)).start() + + busy.join() + first.join() + second.join() + + busy.assert_ok("request against the busy model") + first.assert_ok("first queued request") + second.assert_ok("second queued request") + + assert first.done_at < second.done_at, "queue was not served in arrival order" + + def test_router_no_models_autoload(): global server server.no_models_autoload = True @@ -310,7 +460,7 @@ def _wait_for_sse_event(collected: list, event_type: str, model: str, timeout: i while time.time() < deadline: if any(e.get("event") == event_type and e.get("model") == model for e in collected): return True - time.sleep(0.5) + time.sleep(0.01) return False diff --git a/tools/server/tests/utils.py b/tools/server/tests/utils.py index ae56bc70a..3416f0bfd 100644 --- a/tools/server/tests/utils.py +++ b/tools/server/tests/utils.py @@ -132,7 +132,10 @@ class ServerProcess: self.external_server = "DEBUG_EXTERNAL" in os.environ def start(self, timeout_seconds: int = DEFAULT_HTTP_TIMEOUT) -> None: - env = {**os.environ} + env = { + **os.environ, + "LLAMA_SERVER_DEBUG_FAKE_TIMING": "1", + } if "LLAMA_CACHE" not in os.environ: env["LLAMA_CACHE"] = "tmp" if self.external_server: @@ -303,6 +306,7 @@ class ServerProcess: # wait for server to start start_time = time.time() + last_print_time = start_time while time.time() - start_time < timeout_seconds: try: response = self.make_request("GET", "/health", headers={ @@ -317,8 +321,10 @@ class ServerProcess: if self.process.poll() is not None: raise RuntimeError(f"Server process died with return code {self.process.returncode}") - print(f"Waiting for server to start...") - time.sleep(0.5) + if time.time() - last_print_time >= 1.0: + print(f"Waiting for server to start...") + last_print_time = time.time() + time.sleep(0.01) raise TimeoutError(f"Server did not start within {timeout_seconds} seconds") def stop(self) -> None: diff --git a/tools/tts/tts.cpp b/tools/tts/tts.cpp index b68edcaf5..49c405e14 100644 --- a/tools/tts/tts.cpp +++ b/tools/tts/tts.cpp @@ -179,17 +179,20 @@ int main(int argc, char ** argv) { const char * data = nullptr; size_t data_len = 0; int64_t n_samples = 0; + const int64_t t_wav_start_us = ggml_time_us(); if (gen.get_output(&sample_rate, &data, &data_len, &n_samples) != 0) { LOG_ERR("get_output failed\n"); return 1; } + const double t_wav_s = (ggml_time_us() - t_wav_start_us) / 1e6; LOG_INF("generated %d frames, %zu bytes of WAV audio (%d Hz)\n", n_frames, data_len, sample_rate); const double t_prompt_s = (t_gen_start_us - t_prompt_start_us) / 1e6; - const double t_total_s = t_prompt_s + t_gen_s; + const double t_total_s = t_prompt_s + t_gen_s + t_wav_s; const double audio_s = sample_rate > 0 ? (double) n_samples / sample_rate : 0.0; - LOG_INF("timings: prompt eval %.2fs + generation %.2fs = total %.2fs\n", t_prompt_s, t_gen_s, t_total_s); + LOG_INF("timings: prompt eval %.2fs + generation %.2fs + vocoder %.2fs = total %.2fs\n", + t_prompt_s, t_gen_s, t_wav_s, t_total_s); LOG_INF(" output audio = %.2fs (audio time = %.2fx process time)\n", audio_s, t_total_s > 0 ? audio_s / t_total_s : 0.0); FILE * f = fopen(params.out_file.c_str(), "wb"); if (!f) { diff --git a/tools/ui/.npmrc b/tools/ui/.npmrc index 32e601270..0a690322a 100644 --- a/tools/ui/.npmrc +++ b/tools/ui/.npmrc @@ -1,2 +1,3 @@ engine-strict=true ignore-scripts=true +min-release-age=7 diff --git a/tools/ui/src/app.d.ts b/tools/ui/src/app.d.ts index b9484d950..814fcf523 100644 --- a/tools/ui/src/app.d.ts +++ b/tools/ui/src/app.d.ts @@ -143,10 +143,8 @@ declare global { idxThemeStyle?: number; idxCodeBlock?: number; - // File System Access API - missing from older DOM lib versions. - // Used by ChatFormWorkingDirectory's native folder picker. Feature availability - // is gated at runtime via `typeof window.showDirectoryPicker === 'function'`. - showDirectoryPicker: (options?: { + // File System Access API - not in the DOM lib and unavailable in some browsers + showDirectoryPicker?: (options?: { id?: string; mode?: 'read' | 'readwrite'; startIn?: FileSystemHandle | string; diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte index 105e414fe..1df125708 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte @@ -2,6 +2,7 @@ import { ChatAttachmentsList, ChatFormActions, + ChatFormContenteditable, ChatFormFileInputInvisible, ChatFormMcpResourcesList, ChatFormPickers, @@ -14,9 +15,7 @@ INPUT_CLASSES, SETTING_CONFIG_DEFAULT, INITIAL_FILE_SIZE, - PROMPT_CONTENT_SEPARATOR, - PROMPT_TRIGGER_PREFIX, - RESOURCE_TRIGGER_PREFIX + PROMPT_CONTENT_SEPARATOR } from '$lib/constants'; import { ContentPartType, @@ -39,8 +38,25 @@ activeConversation, pendingCwd } from '$lib/stores/conversations.svelte'; - import type { GetPromptResult, MCPPromptInfo, MCPResourceInfo, PromptMessage } from '$lib/types'; - import { isIMEComposing, parseClipboardContent, uuid } from '$lib/utils'; + import type { + FileMentionEntry, + GetPromptResult, + MCPPromptInfo, + MCPResourceInfo, + PromptMessage + } from '$lib/types'; + import { + buildMentionInsertion, + containsCodeSpan, + containsFileMentionLink, + findCommandToken, + findMentionToken, + isIMEComposing, + isOffsetInCodeBlock, + parseClipboardContent, + uuid + } from '$lib/utils'; + import { useChatFormPickers } from '$lib/hooks/use-chat-form-pickers.svelte'; import { AudioRecorder, convertToWav, @@ -97,29 +113,67 @@ }: Props = $props(); // Component References + // Shared handle of the two input renderers (textarea + contenteditable). + type ChatInputHandle = { + focus(): void; + resetHeight(): void; + getElement(): HTMLElement | undefined; + getCaretOffset(): number; + setCaretOffset(offset: number): void; + }; + let audioRecorder: AudioRecorder | undefined; let chatFormActionsRef: ChatFormActions | undefined = $state(undefined); let fileInputRef: ChatFormFileInputInvisible | undefined = $state(undefined); let pickersRef: { handleKeydown: (event: KeyboardEvent) => boolean } | undefined = $state(undefined); - let textareaRef: ChatFormTextarea | undefined = $state(undefined); + let inputRef: ChatInputHandle | undefined = $state(undefined); + + // Render-mode gate: the plain textarea by default, the contenteditable + // while the buffer carries a `file://` mention link or a complete code + // span (badges and code chips need a DOM the textarea cannot provide). + // Demotes back once neither remains. + let useContenteditable = $state(false); // Audio Recording State let isRecording = $state(false); let recordingSupported = $state(false); - // Picker State - let isPromptPickerOpen = $state(false); - let promptSearchQuery = $state(''); - let isInlineResourcePickerOpen = $state(false); - let resourceSearchQuery = $state(''); + // Invisible anchor at the form's top edge so the mention/WD popovers + // float above the box. + let mentionAnchor: HTMLDivElement | null = $state(null); let cwd = $derived(activeConversation()?.cwd ?? pendingCwd()); - async function handleWorkingDirectoryChange(value: string | null) { - await conversationsStore.setCwd(value); + const pickers = useChatFormPickers({ + getValue: () => value, + setValue: (v) => { + value = v; + onValueChange?.(v); + }, + getCaretOffset: () => inputRef?.getCaretOffset(), + setCaretOffset: (offset) => inputRef?.setCaretOffset(offset), + focusInput: refocusInput, + getShowModelSelector: () => showModelSelector, + hasPrompts: () => mcpStore.hasPromptsCapability(conversationsStore.getAllMcpServerOverrides()), + hasBuiltinTools: () => toolsStore.builtinTools.length > 0, + getCwd: () => cwd, + getServerHome: () => toolsStore.serverHome ?? null, + openModelSelector: () => chatFormActionsRef?.openModelSelector(), + getPickersRef: () => pickersRef + }); + + async function handleWorkingDirectoryChange(newDir: string | null) { + // Committing a directory consumes the `/cwd` token; the chip's + // clear-X path has no token to consume. + const token = findCommandToken(value); + if (token && token.name === 'cwd') { + value = ''; + onValueChange?.(''); + } + await conversationsStore.setCwd(newDir); if (conversationsStore.activeConversation) { - await chatStore.recordCwdChange(value?.trim() || null); + await chatStore.recordCwdChange(newDir?.trim() || null); } } @@ -166,23 +220,45 @@ ); let canSubmit = $derived(value.trim().length > 0 || hasAttachments); + // Caret offset restored after a renderer swap. Callers that mutate + // `value` themselves (e.g. the mention picker) pin the target offset + // BEFORE the assignment; otherwise the swap effect snapshots the + // current caret. + let pendingCaretOffset = 0; + let caretOffsetPinned = false; + + function queueCaretRestore() { + queueMicrotask(() => { + inputRef?.focus(); + inputRef?.setCaretOffset(pendingCaretOffset); + caretOffsetPinned = false; + }); + } + + $effect(() => { + const wantContenteditable = + containsFileMentionLink(value ?? '') || containsCodeSpan(value ?? ''); + if (useContenteditable === wantContenteditable) return; + + if (!caretOffsetPinned) { + pendingCaretOffset = inputRef?.getCaretOffset() ?? (value ?? '').length; + } + + useContenteditable = wantContenteditable; + queueCaretRestore(); + }); + onMount(() => { recordingSupported = isAudioRecordingSupported(); audioRecorder = new AudioRecorder(); }); - // Defer so the closing popover's focus scope tears down first - bits-ui - // yanks a synchronous focus() back into the still-mounted popover. - function refocusInput() { - queueMicrotask(() => textareaRef?.focus()); - } - export function focus() { - textareaRef?.focus(); + inputRef?.focus(); } export function resetTextareaHeight() { - textareaRef?.resetHeight(); + inputRef?.resetHeight(); } export function openModelSelector() { @@ -216,46 +292,10 @@ } } - function handleInput() { - const perChatOverrides = conversationsStore.getAllMcpServerOverrides(); - const hasServers = mcpStore.hasEnabledServers(perChatOverrides); - - if (value.startsWith(PROMPT_TRIGGER_PREFIX) && hasServers) { - isPromptPickerOpen = true; - promptSearchQuery = value.slice(1); - isInlineResourcePickerOpen = false; - resourceSearchQuery = ''; - } else if ( - value.startsWith(RESOURCE_TRIGGER_PREFIX) && - hasServers && - mcpStore.hasResourcesCapability(perChatOverrides) - ) { - isInlineResourcePickerOpen = true; - resourceSearchQuery = value.slice(1); - isPromptPickerOpen = false; - promptSearchQuery = ''; - } else { - isPromptPickerOpen = false; - promptSearchQuery = ''; - isInlineResourcePickerOpen = false; - resourceSearchQuery = ''; - } - } - function handleKeydown(event: KeyboardEvent) { - if (pickersRef?.handleKeydown(event)) { - return; - } - - if (event.key === KeyboardKey.ESCAPE && isPromptPickerOpen) { - isPromptPickerOpen = false; - promptSearchQuery = ''; - return; - } - - if (event.key === KeyboardKey.ESCAPE && isInlineResourcePickerOpen) { - isInlineResourcePickerOpen = false; - resourceSearchQuery = ''; + // Pickers consume navigation/escape keys first; when consumed, skip + // the enter-to-submit logic below. + if (pickers.handleKeydown(event)) { return; } @@ -263,6 +303,15 @@ const isModifier = event.ctrlKey || event.metaKey; const sendOnEnter = currentConfig.sendOnEnter !== false; + // Caret inside a fenced code block (closed, or still open + // while being typed): Enter adds a line, never submits. The + // contenteditable consumes this case locally; this gate + // covers the plain textarea, where skipping submit lets the + // native newline through. + if (!isModifier && isOffsetInCodeBlock(value ?? '', inputRef?.getCaretOffset() ?? 0)) { + return; + } + if (sendOnEnter || isModifier) { event.preventDefault(); @@ -332,7 +381,7 @@ } setTimeout(() => { - textareaRef?.focus(); + inputRef?.focus(); }, 10); return; @@ -359,13 +408,7 @@ promptInfo: MCPPromptInfo, args?: Record ) { - // Only clear the value if the prompt was triggered by typing '/' - if (value.startsWith(PROMPT_TRIGGER_PREFIX)) { - value = ''; - onValueChange?.(''); - } - isPromptPickerOpen = false; - promptSearchQuery = ''; + pickers.closePromptPicker(); const promptName = promptInfo.title || promptInfo.name; const placeholder: ChatUploadedFile = { @@ -384,7 +427,7 @@ uploadedFiles = [...uploadedFiles, placeholder]; onUploadedFilesChange?.(uploadedFiles); - textareaRef?.focus(); + inputRef?.focus(); } function handlePromptLoadComplete(placeholderId: string, result: GetPromptResult) { @@ -426,39 +469,36 @@ onUploadedFilesChange?.(uploadedFiles); } - function handlePromptPickerClose() { - isPromptPickerOpen = false; - promptSearchQuery = ''; - textareaRef?.focus(); + // Deferred so the closing popover's focus scope tears down first - + // bits-ui yanks a synchronous focus() back into the still-mounted popover. + function refocusInput() { + queueMicrotask(() => inputRef?.focus()); } - function handleInlineResourcePickerClose() { - isInlineResourcePickerOpen = false; - resourceSearchQuery = ''; - textareaRef?.focus(); - } + // Splice the mention link in place of the `@` token. Uses the + // live cursor, not a stale snapshot - the token may have been edited. + function handleMentionSelect(entry: FileMentionEntry) { + const cursor = inputRef?.getCaretOffset() ?? value.length; + const token = findMentionToken(value, cursor); + if (!token) return; - function handleInlineResourceSelect() { - if (value.startsWith(RESOURCE_TRIGGER_PREFIX)) { - value = ''; - onValueChange?.(''); + const built = buildMentionInsertion(entry, value, token); + if (!built) return; + + // Pin the post-insertion caret BEFORE the swap effect runs; + // otherwise the effect clobbers it with the textarea's selection + // at promotion time (browser-dependent: usually reset to 0). + pendingCaretOffset = built.caretOffset; + caretOffsetPinned = true; + + value = built.newValue; + onValueChange?.(built.newValue); + + // Already in contenteditable mode: no renderer flip, so the swap + // effect's caret restore never runs. + if (useContenteditable) { + queueCaretRestore(); } - - isInlineResourcePickerOpen = false; - resourceSearchQuery = ''; - textareaRef?.focus(); - } - - function handleBrowseResources() { - isInlineResourcePickerOpen = false; - resourceSearchQuery = ''; - - if (value.startsWith(RESOURCE_TRIGGER_PREFIX)) { - value = ''; - onValueChange?.(''); - } - - isResourceDialogOpen = true; } async function handleMicClick() { @@ -503,19 +543,32 @@ > inputRef?.focus()} + onMentionSelect={handleMentionSelect} onPromptLoadStart={handlePromptLoadStart} onPromptLoadComplete={handlePromptLoadComplete} onPromptLoadError={handlePromptLoadError} - onInlineResourceBrowse={handleBrowseResources} /> + +
- { - handleInput(); - onValueChange?.(value); - }} - {disabled} - {placeholder} - /> + {#if useContenteditable} + { + pickers.handleInput(); + onValueChange?.(value); + }} + onPaste={handlePaste} + {disabled} + {placeholder} + /> + {:else} + { + pickers.handleInput(); + onValueChange?.(value); + }} + onPaste={handlePaste} + {disabled} + {placeholder} + /> + {/if} {#if mcpHasResourceAttachments()} onSystemPromptClick?.({ message: value, files: uploadedFiles })} - onMcpPromptClick={showMcpPromptButton ? () => (isPromptPickerOpen = true) : undefined} + onMcpPromptClick={showMcpPromptButton ? () => pickers.openPromptPicker() : undefined} onMcpResourcesClick={() => (isResourceDialogOpen = true)} />
@@ -585,8 +654,12 @@ {#if toolsStore.builtinTools.length > 0} {/if} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContenteditable.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContenteditable.svelte new file mode 100644 index 000000000..18fd24302 --- /dev/null +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContenteditable.svelte @@ -0,0 +1,788 @@ + + +
+
+
+ + diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormCommandPicker.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormCommandPicker.svelte new file mode 100644 index 000000000..5feaaf35a --- /dev/null +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormCommandPicker.svelte @@ -0,0 +1,135 @@ + + + + command.name} + scrollTrigger={nav.scrollTrigger} + > + {#snippet item(command, index, isSelected)} + {@const Icon = commandIcon[command.action]} + handleSelect(command)} + onmouseenter={() => { + if (!command.disabled) nav.setHover(index); + }} + > + +
+ /{command.name} + + {command.description} + +
+
+ {/snippet} +
+
diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte new file mode 100644 index 000000000..07cbaf492 --- /dev/null +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte @@ -0,0 +1,269 @@ + + + { + if (!open) onClose(); + }} +> + + + + event.preventDefault()} + onCloseAutoFocus={(event) => event.preventDefault()} + class={[ + 'w-[var(--bits-popover-anchor-width)] max-w-none rounded-xl border-border/50 p-0 shadow-xl', + className + ]} + > + entry.type + ':' + entry.path} + scrollTrigger={nav.scrollTrigger} + > + {#snippet item(entry, index, isSelected)} + handleSelect(entry)} + onmouseenter={() => nav.setHover(index)} + > + {@const Icon = entry.type === FileMentionEntryType.DIRECTORY ? Folder : File} + +
+
+ {#if showTooltip} + + + {#snippet child({ props })} + {entry.name} + {/snippet} + + +

{entry.path}

+
+
+ {:else} + {entry.name} + {/if} + + {entry.type} + +
+ + + +
+
+ {/snippet} +
+
+
diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerList.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerList.svelte index 6647928b2..04b85b24b 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerList.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerList.svelte @@ -2,6 +2,7 @@ import type { Snippet } from 'svelte'; import { SearchInput } from '$lib/components/app'; import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte'; + import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte'; import { CHAT_FORM_POPOVER_MAX_HEIGHT } from '$lib/constants'; interface Props { @@ -11,11 +12,19 @@ searchQuery: string; showSearchInput: boolean; searchPlaceholder?: string; + // Omit to distinguish "haven't searched yet" from "search returned nothing". emptyMessage?: string; + autofocus?: boolean; + inputRef?: HTMLInputElement | null; + onSearchClose?: () => void; itemKey: (item: T, index: number) => string; item: Snippet<[T, number, boolean]>; skeleton?: Snippet; + skeletonCount?: number; footer?: Snippet; + // Counter bumped by the picker on keyboard nav; scrolls the selected + // row into view without scrolling on hover or result replacement. + scrollTrigger?: number; } let { @@ -25,49 +34,69 @@ searchQuery = $bindable(), showSearchInput, searchPlaceholder = 'Search...', - emptyMessage = 'No items available', + emptyMessage, + autofocus = false, + inputRef = $bindable(null), + onSearchClose, itemKey, item, skeleton, - footer + skeletonCount = 6, + footer, + scrollTrigger }: Props = $props(); let listContainer = $state(null); - $effect(() => { - if (listContainer && selectedIndex >= 0 && selectedIndex < items.length) { - const selectedElement = listContainer.querySelector( - `[data-picker-index="${selectedIndex}"]` - ) as HTMLElement; + let listPaddingTop = $derived( + showSearchInput ? (isLoading || items.length > 0 ? 'pt-13' : 'pt-10') : '' + ); - if (selectedElement) { - selectedElement.scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'nearest' - }); - } - } + // selectedIndex/items.length are untracked so hover and result replacement + // never re-fire the scroll; keyboard nav is the only path that bumps the trigger. + useScrollActiveRow({ + getTrigger: () => scrollTrigger, + getContainer: () => listContainer, + getIndex: () => selectedIndex, + getCount: () => items.length, + dataIndex: 'picker' }); {#if showSearchInput}
- +
{/if} -
+
{#if isLoading} {#if skeleton} {@render skeleton()} + {:else} +
+ {#each { length: skeletonCount } as _, rowIndex (rowIndex)} +
+
+
+
+
+
+
+ {/each} +
+ {/if} + {:else if items && items.length === 0} + {#if emptyMessage} +
{emptyMessage}
{/if} - {:else if items.length === 0} -
{emptyMessage}
{:else} {#each items as itemData, index (itemKey(itemData, index))} {@render item(itemData, index, index === selectedIndex)} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerListItem.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerListItem.svelte index 4d82c6b58..0fb0ea0c0 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerListItem.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerListItem.svelte @@ -3,21 +3,34 @@ interface Props { isSelected?: boolean; + disabled?: boolean; onclick: () => void; + onmouseenter?: () => void; dataIndex?: number; children: Snippet; + class?: string; } - let { isSelected = false, onclick, dataIndex, children }: Props = $props(); + let { + class: className = '', + isSelected = false, + disabled = false, + onclick, + onmouseenter, + dataIndex, + children + }: Props = $props(); diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerPopover.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerPopover.svelte index c43a002e6..24ea8619f 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerPopover.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerPopover.svelte @@ -42,6 +42,7 @@ align="start" sideOffset={12} class="w-[var(--bits-popover-anchor-width)] max-w-none rounded-xl border-border/50 p-0 shadow-xl {className}" + preventScroll={false} onkeydown={onKeydown} onOpenAutoFocus={(event) => event.preventDefault()} > diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte index f35d816de..22ec53119 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte @@ -45,6 +45,9 @@ let promptArgs = $state>({}); let selectedIndex = $state(0); let internalSearchQuery = $state(''); + // Bumped on ArrowUp/ArrowDown only, so the list scrolls on keyboard + // nav but not on hover or result changes. + let scrollTrigger = $state(0); let promptError = $state(null); let selectedIndexBeforeArgumentForm = $state(null); @@ -295,6 +298,7 @@ event.preventDefault(); if (filteredPrompts.length > 0) { selectedIndex = (selectedIndex + 1) % filteredPrompts.length; + scrollTrigger++; } return true; @@ -304,6 +308,7 @@ event.preventDefault(); if (filteredPrompts.length > 0) { selectedIndex = selectedIndex === 0 ? filteredPrompts.length - 1 : selectedIndex - 1; + scrollTrigger++; } return true; @@ -400,6 +405,7 @@ searchPlaceholder="Search prompts..." emptyMessage="No MCP prompts available" itemKey={(prompt) => prompt.serverName + ':' + prompt.name} + {scrollTrigger} > {#snippet item(prompt, index, isSelected)} {@const server = serverSettingsMap.get(prompt.serverName)} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpResources.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpResources.svelte deleted file mode 100644 index ed97e1fc7..000000000 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpResources.svelte +++ /dev/null @@ -1,237 +0,0 @@ - - - - resource.serverName + ':' + resource.uri} - > - {#snippet item(resource, index, isSelected)} - {@const server = serverSettingsMap.get(resource.serverName)} - {@const serverLabel = server ? mcpStore.getServerLabel(server) : resource.serverName} - - handleResourceClick(resource)} - > - - {#snippet titleExtra()} - {#if isResourceAttached(resource.uri)} - - attached - - {/if} - {/snippet} - - {#snippet subtitle()} -

- {resource.uri} -

- {/snippet} -
-
- {/snippet} - - {#snippet skeleton()} - - {/snippet} - - {#snippet footer()} - {#if onBrowse && resources.length > 3} - - {/if} - {/snippet} -
-
diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickers.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickers.svelte index 7c5dc85b2..0d6cdcbfd 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickers.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickers.svelte @@ -1,16 +1,30 @@ + {})} + onSelect={onCommandSelect ?? (() => {})} +/> + - {})} + onOpened={onMentionOpened} + onSelect={onMentionSelect ?? (() => {})} /> diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormTextarea.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormTextarea.svelte index 3e683389f..8ef56d7ee 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormTextarea.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormTextarea.svelte @@ -28,11 +28,10 @@ onMount(() => { if (textareaElement) { autoResizeTextarea(textareaElement); - textareaElement.focus(); + textareaElement.focus({ preventScroll: true }); } }); - // Expose the textarea element for external access export function getElement() { return textareaElement; } @@ -48,6 +47,17 @@ textareaElement.style.height = '1rem'; } } + + // Plain-text caret offsets, shared with the contenteditable variant so + // the picker/paste flows can address either renderer through one handle. + export function getCaretOffset(): number { + if (!textareaElement) return 0; + return textareaElement.selectionStart ?? textareaElement.value.length; + } + + export function setCaretOffset(offset: number) { + textareaElement?.setSelectionRange(offset, offset); + }
diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectory.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectory.svelte index f6ac9e0e8..f8069c93e 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectory.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectory.svelte @@ -1,7 +1,5 @@ -
- - - + + + + + + event.preventDefault()} + onCloseAutoFocus={(event) => event.preventDefault()} + class="w-[var(--bits-popover-anchor-width)] max-w-none rounded-xl border-border/50 p-0 shadow-xl" + > +
+ - - event.preventDefault()} - > -
- 0 || searchError)} + nav.setHover(index)} /> + {/if} - {#if inputValue.trim() && (isSearching || queryResults.length > 0 || searchError)} - (hoveredIndex = index)} - /> - {/if} + {#if pickerSupported} + + {/if} - {#if pickerSupported} -
+ + + Searching in: + + {abbreviateHome(searchScope, homeBase)} - - Browse - - {/if} - - {#if homeBase} - - - - Searching in: - - {abbreviateHome(searchScope, homeBase)} - - {/if} -
-
-
-
+ + {/if} +
+ + diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectoryChip.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectoryChip.svelte index 4f8d0f7f7..2882f557c 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectoryChip.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectoryChip.svelte @@ -1,6 +1,7 @@ + +{#each segments as seg, i (i)} + {#if seg.match} + {seg.text} + {:else} + {seg.text} + {/if} +{/each} diff --git a/tools/ui/src/lib/components/app/forms/index.ts b/tools/ui/src/lib/components/app/forms/index.ts index 4cf56cdc9..87594d7e3 100644 --- a/tools/ui/src/lib/components/app/forms/index.ts +++ b/tools/ui/src/lib/components/app/forms/index.ts @@ -42,3 +42,11 @@ export { default as KeyValuePairs } from './KeyValuePairs.svelte'; * Supports placeholder, autofocus, and change callbacks. */ export { default as SearchInput } from './SearchInput.svelte'; + +/** + * **HighlightedMatch** - Substring-match text highlight + * + * Renders `text` with each case-insensitive occurrence of `query` wrapped + * in ``. + */ +export { default as HighlightedMatch } from './HighlightedMatch.svelte'; diff --git a/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte b/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte index 720963a8d..415e5164d 100644 --- a/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte +++ b/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte @@ -1,8 +1,9 @@ + + diff --git a/tools/ui/tests/client/components/ChatFormPickersHarness.svelte b/tools/ui/tests/client/components/ChatFormPickersHarness.svelte new file mode 100644 index 000000000..76b2e9fe3 --- /dev/null +++ b/tools/ui/tests/client/components/ChatFormPickersHarness.svelte @@ -0,0 +1,51 @@ + diff --git a/tools/ui/tests/client/components/ChatFormTestWrapper.svelte b/tools/ui/tests/client/components/ChatFormTestWrapper.svelte new file mode 100644 index 000000000..78a431157 --- /dev/null +++ b/tools/ui/tests/client/components/ChatFormTestWrapper.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/tools/ui/tests/client/components/PickerListScrollHarness.svelte b/tools/ui/tests/client/components/PickerListScrollHarness.svelte new file mode 100644 index 000000000..87746979f --- /dev/null +++ b/tools/ui/tests/client/components/PickerListScrollHarness.svelte @@ -0,0 +1,43 @@ + + +
conversation
+ +{#if open} +
+ it.id} + > + {#snippet item(it, index, isSelected)} + {}}> + {it.label} + + {/snippet} + +
+{/if} diff --git a/tools/ui/tests/client/picker-list-scroll.svelte.test.ts b/tools/ui/tests/client/picker-list-scroll.svelte.test.ts new file mode 100644 index 000000000..1ce019985 --- /dev/null +++ b/tools/ui/tests/client/picker-list-scroll.svelte.test.ts @@ -0,0 +1,29 @@ +// Regression test: opening a chat-form picker must not scroll the +// conversation to the top. Root cause: the list's scroll effect fired +// scrollIntoView on the initial mount, before the popover was positioned, +// so the browser scrolled every scrollable ancestor to reveal the row. + +import { describe, it, expect } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import { tick } from 'svelte'; +import PickerListScrollHarness from './components/PickerListScrollHarness.svelte'; + +describe('ChatFormPickerList mount scroll', () => { + it('does not scroll documentElement when the picker mounts', async () => { + const screen = render(PickerListScrollHarness); + await tick(); + + document.documentElement.scrollTop = document.documentElement.scrollHeight; + await tick(); + const before = document.documentElement.scrollTop; + expect(before).toBeGreaterThan(0); + + screen.component.openPicker(); + await tick(); + await new Promise((r) => setTimeout(r, 100)); + await tick(); + + const after = document.documentElement.scrollTop; + expect(after).toBe(before); + }); +}); diff --git a/tools/ui/tests/client/settings-raw-text-migration.svelte.test.ts b/tools/ui/tests/client/settings-raw-text-migration.svelte.test.ts new file mode 100644 index 000000000..601ebc2b3 --- /dev/null +++ b/tools/ui/tests/client/settings-raw-text-migration.svelte.test.ts @@ -0,0 +1,65 @@ +// Guards the legacy render-key migration: `renderUserContentAsMarkdown` +// and `renderThinkingAsMarkdown` (opt-INTO markdown) fold into the single +// `renderContentAsRawText` setting, with any explicit raw-text preference +// winning when the legacy keys disagree. Legacy keys are removed from the +// persisted config so they do not stay orphaned in localStorage. + +import { beforeEach, describe, expect, it } from 'vitest'; +import { settingsStore, config } from '$lib/stores/settings.svelte'; +import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage'; + +function seedConfig(stored: Record) { + localStorage.setItem(CONFIG_LOCALSTORAGE_KEY, JSON.stringify(stored)); + settingsStore.initialize(); +} + +function persisted(): Record { + return JSON.parse(localStorage.getItem(CONFIG_LOCALSTORAGE_KEY) ?? '{}'); +} + +describe('renderContentAsRawText migration', () => { + beforeEach(() => { + localStorage.removeItem(CONFIG_LOCALSTORAGE_KEY); + settingsStore.initialize(); + }); + + it('maps renderUserContentAsMarkdown=false to raw text', () => { + seedConfig({ renderUserContentAsMarkdown: false }); + expect(config().renderContentAsRawText).toBe(true); + }); + + it('maps renderUserContentAsMarkdown=true to markdown', () => { + seedConfig({ renderUserContentAsMarkdown: true }); + expect(config().renderContentAsRawText).toBe(false); + }); + + it('maps renderThinkingAsMarkdown=false to raw text', () => { + seedConfig({ renderThinkingAsMarkdown: false }); + expect(config().renderContentAsRawText).toBe(true); + }); + + it('lets any explicit raw-text preference win when the legacy keys disagree', () => { + seedConfig({ renderUserContentAsMarkdown: true, renderThinkingAsMarkdown: false }); + expect(config().renderContentAsRawText).toBe(true); + }); + + it('honors the intermediate renderUserContentAsRawText key from the PR branch', () => { + seedConfig({ renderUserContentAsRawText: true }); + expect(config().renderContentAsRawText).toBe(true); + }); + + it('keeps an already-migrated value and cleans up the legacy keys', () => { + seedConfig({ renderContentAsRawText: false, renderUserContentAsMarkdown: false }); + expect(config().renderContentAsRawText).toBe(false); + + const stored = persisted(); + expect(stored.renderUserContentAsMarkdown).toBeUndefined(); + expect(stored.renderThinkingAsMarkdown).toBeUndefined(); + expect(stored.renderUserContentAsRawText).toBeUndefined(); + }); + + it('defaults to markdown when no legacy key exists', () => { + seedConfig({}); + expect(config().renderContentAsRawText).toBe(false); + }); +}); diff --git a/tools/ui/tests/unit/code.test.ts b/tools/ui/tests/unit/code.test.ts index 7820c9807..99fad6044 100644 --- a/tools/ui/tests/unit/code.test.ts +++ b/tools/ui/tests/unit/code.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { highlightCode, trimCodePadding } from '$lib/utils/code'; +import { highlightCode, splitGluedClosingCodeFences, trimCodePadding } from '$lib/utils/code'; describe('trimCodePadding', () => { it('removes a single leading newline', () => { @@ -101,3 +101,38 @@ describe('highlightCode', () => { expect(html).toBe('<script>a && b</script>'); }); }); + +describe('splitGluedClosingCodeFences', () => { + it('splits text glued to a closing fence onto its own line', () => { + const input = "```ts\nlet foo = 'bar';\n```create this file on [Desktop](file:///a/b/)"; + expect(splitGluedClosingCodeFences(input)).toBe( + "```ts\nlet foo = 'bar';\n```\ncreate this file on [Desktop](file:///a/b/)" + ); + }); + + it('leaves a well-formed code block untouched', () => { + const input = "```ts\nlet foo = 'bar';\n```\ncreate this file on [Desktop](file:///a/b/)"; + expect(splitGluedClosingCodeFences(input)).toBe(input); + }); + + it('leaves content without fences untouched', () => { + expect(splitGluedClosingCodeFences('hello world')).toBe('hello world'); + }); + + it('keeps nested markdown fences inside a block intact', () => { + const input = '```md\n# Example\n```python\nprint(1)\n```\n```'; + expect(splitGluedClosingCodeFences(input)).toBe(input); + }); + + it('splits every glued closing fence when several blocks are present', () => { + const input = '```ts\na\n```first words\n\n```js\nb\n```second words'; + expect(splitGluedClosingCodeFences(input)).toBe( + '```ts\na\n```\nfirst words\n\n```js\nb\n```\nsecond words' + ); + }); + + it('leaves a still-open fence untouched', () => { + const input = '```ts\nlet foo = 1;'; + expect(splitGluedClosingCodeFences(input)).toBe(input); + }); +}); diff --git a/tools/ui/tests/unit/command-token.test.ts b/tools/ui/tests/unit/command-token.test.ts new file mode 100644 index 000000000..f52097de5 --- /dev/null +++ b/tools/ui/tests/unit/command-token.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest'; +import { findCommandToken, takeCommandDismissSnapshot } from '$lib/utils'; + +describe('findCommandToken', () => { + it('returns null when the value does not start with a slash', () => { + expect(findCommandToken('hello /prompt')).toBeNull(); + expect(findCommandToken('')).toBeNull(); + expect(findCommandToken('prompt')).toBeNull(); + }); + + it('parses a bare slash', () => { + expect(findCommandToken('/')).toEqual({ name: '', args: '', end: 1 }); + }); + + it('parses a command name with no args', () => { + expect(findCommandToken('/prompt')).toEqual({ name: 'prompt', args: '', end: 7 }); + }); + + it('parses a command name followed by a space', () => { + expect(findCommandToken('/prompt ')).toEqual({ name: 'prompt', args: '', end: 8 }); + }); + + it('parses args after the command name', () => { + expect(findCommandToken('/prompt rev')).toEqual({ name: 'prompt', args: 'rev', end: 11 }); + }); + + it('parses multi-word args', () => { + expect(findCommandToken('/prompt review code ')).toEqual({ + name: 'prompt', + args: ' review code ', + end: 22 + }); + }); + + it('treats the whole run as the name when there is no space', () => { + expect(findCommandToken('/promptx')).toEqual({ name: 'promptx', args: '', end: 8 }); + }); +}); + +describe('takeCommandDismissSnapshot', () => { + it('returns null when there is no command token', () => { + expect(takeCommandDismissSnapshot('hello')).toBeNull(); + }); + + it('captures the name and args', () => { + expect(takeCommandDismissSnapshot('/prompt rev')).toEqual({ + name: 'prompt', + args: 'rev' + }); + }); +}); diff --git a/tools/ui/tests/unit/contenteditable-tokenizer.test.ts b/tools/ui/tests/unit/contenteditable-tokenizer.test.ts new file mode 100644 index 000000000..8752415f0 --- /dev/null +++ b/tools/ui/tests/unit/contenteditable-tokenizer.test.ts @@ -0,0 +1,205 @@ +import { describe, expect, it } from 'vitest'; +import { containsCodeSpan, isOffsetInCodeBlock, tokenizeContent } from '$lib/utils'; + +describe('tokenizeContent', () => { + it('tokenizes a plain text buffer with no badges', () => { + expect(tokenizeContent('hello world')).toEqual([{ kind: 'text', text: 'hello world' }]); + }); + + it('tokenizes a single badge', () => { + expect(tokenizeContent('[docs](file:///a/b)')).toEqual([ + { kind: 'badge', name: 'docs', path: '/a/b' } + ]); + }); + + it('tokenizes text around a single badge', () => { + expect(tokenizeContent('hello [docs](file:///a/b) world')).toEqual([ + { kind: 'text', text: 'hello ' }, + { kind: 'badge', name: 'docs', path: '/a/b' }, + { kind: 'text', text: ' world' } + ]); + }); + + it('tokenizes adjacent badges as separate tokens', () => { + expect(tokenizeContent('[a](file:///x)[b](file:///y)')).toEqual([ + { kind: 'badge', name: 'a', path: '/x' }, + { kind: 'badge', name: 'b', path: '/y' } + ]); + }); + + it('leaves non-file links untouched in the stream', () => { + expect(tokenizeContent('see [foo](https://example.com) for details')).toEqual([ + { kind: 'text', text: 'see [foo](https://example.com) for details' } + ]); + }); + + it('recognizes badges whose path contains spaces (macOS screenshots)', () => { + const path = '/Users/allozaur/Desktop/Screenshot 2026-07-28 at 17.21.50.png'; + const source = `[Screenshot 2026-07-28 at 17.21.50.png](file://${path}) `; + expect(tokenizeContent(source)).toEqual([ + { kind: 'badge', name: 'Screenshot 2026-07-28 at 17.21.50.png', path }, + { kind: 'text', text: ' ' } + ]); + }); + + it('recognizes badges whose path lives in the macOS temp folder', () => { + const path = + '/var/folders/78/j28m7pn57wb34bfjwlskh62h0000gn/T/TemporaryItems/NSIRD_screencaptureui_GD0A2R/Screenshot 2026-07-28 at 17.23.28.png'; + const source = `[Screenshot 2026-07-28 at 17.23.28.png](file://${path}) `; + expect(tokenizeContent(source)).toEqual([ + { kind: 'badge', name: 'Screenshot 2026-07-28 at 17.23.28.png', path }, + { kind: 'text', text: ' ' } + ]); + }); + + it('keeps text around a badge with spaces in the path', () => { + const path = '/Users/allozaur/Desktop/Screenshot 2026-07-28 at 17.21.50.png'; + const source = `see [Screenshot 2026-07-28 at 17.21.50.png](file://${path}) done`; + expect(tokenizeContent(source)).toEqual([ + { kind: 'text', text: 'see ' }, + { kind: 'badge', name: 'Screenshot 2026-07-28 at 17.21.50.png', path }, + { kind: 'text', text: ' done' } + ]); + }); + + it('recognizes badges whose path contains a close parenthesis (macOS duplicate files)', () => { + const path = '/Users/foo/Screenshot (1).png'; + const source = `[Screenshot (1).png](file://${path}) `; + expect(tokenizeContent(source)).toEqual([ + { kind: 'badge', name: 'Screenshot (1).png', path }, + { kind: 'text', text: ' ' } + ]); + }); + + it('recognizes badges whose folder name is wrapped in parentheses', () => { + const path = '/Users/foo/Project (Stuff)/main.rs'; + const source = `[main.rs](file://${path}) `; + expect(tokenizeContent(source)).toEqual([ + { kind: 'badge', name: 'main.rs', path }, + { kind: 'text', text: ' ' } + ]); + }); + + it('recognizes adjacent badges back-to-back with no separator', () => { + const source = '[a](file:///p)[b](file:///q)'; + expect(tokenizeContent(source)).toEqual([ + { kind: 'badge', name: 'a', path: '/p' }, + { kind: 'badge', name: 'b', path: '/q' } + ]); + }); + + it('tokenizes inline code with the backticks included', () => { + expect(tokenizeContent('run `npm test` now')).toEqual([ + { kind: 'text', text: 'run ' }, + { kind: 'inlineCode', text: '`npm test`' }, + { kind: 'text', text: ' now' } + ]); + }); + + it('tokenizes a fenced code block without a language', () => { + const source = 'before\n```\nconst a = 1;\n```\nafter'; + expect(tokenizeContent(source)).toEqual([ + { kind: 'text', text: 'before\n' }, + { kind: 'codeBlock', text: '```\nconst a = 1;\n```' }, + { kind: 'text', text: '\nafter' } + ]); + }); + + it('tokenizes a fenced code block with a language', () => { + const source = '```js\nconst a = 1;\n```'; + expect(tokenizeContent(source)).toEqual([ + { kind: 'codeBlock', text: '```js\nconst a = 1;\n```' } + ]); + }); + + it('prefers the fenced block over inline spans at triple backticks', () => { + expect(tokenizeContent('```a``` ```b```')).toEqual([ + { kind: 'codeBlock', text: '```a```' }, + { kind: 'text', text: ' ' }, + { kind: 'codeBlock', text: '```b```' } + ]); + }); + + it('leaves an unclosed fence as plain text', () => { + expect(tokenizeContent('```js\nconst a = 1;')).toEqual([ + { kind: 'text', text: '```js\nconst a = 1;' } + ]); + }); + + it('leaves an unclosed inline backtick as plain text', () => { + expect(tokenizeContent('run `npm test')).toEqual([{ kind: 'text', text: 'run `npm test' }]); + }); + + it('does not recognize badges inside code spans', () => { + expect(tokenizeContent('`[a](file:///p)`')).toEqual([ + { kind: 'inlineCode', text: '`[a](file:///p)`' } + ]); + }); + + it('tokenizes badges and code spans side by side', () => { + expect(tokenizeContent('[a](file:///p) `x`')).toEqual([ + { kind: 'badge', name: 'a', path: '/p' }, + { kind: 'text', text: ' ' }, + { kind: 'inlineCode', text: '`x`' } + ]); + }); +}); + +describe('containsCodeSpan', () => { + it('detects inline code', () => { + expect(containsCodeSpan('run `npm test` now')).toBe(true); + }); + + it('detects a fenced block with a language', () => { + expect(containsCodeSpan('```js\nconst a = 1;\n```')).toBe(true); + }); + + it('detects a fenced block without a language', () => { + expect(containsCodeSpan('```\ncode\n```')).toBe(true); + }); + + it('ignores unclosed fences and lone backticks', () => { + expect(containsCodeSpan('```js\nconst a = 1;')).toBe(false); + expect(containsCodeSpan('run `npm test')).toBe(false); + expect(containsCodeSpan('``')).toBe(false); + }); + + it('ignores plain text and mention links', () => { + expect(containsCodeSpan('hello world')).toBe(false); + expect(containsCodeSpan('[a](file:///p)')).toBe(false); + }); +}); + +describe('isOffsetInCodeBlock', () => { + const BLOCK = '```js\nconst a = 1;\n```'; + + it('is false with no fences in the buffer', () => { + expect(isOffsetInCodeBlock('hello world', 5)).toBe(false); + expect(isOffsetInCodeBlock('run `npm test` now', 10)).toBe(false); + }); + + it('is true right after the opening fence, before any content', () => { + expect(isOffsetInCodeBlock('```', 3)).toBe(true); + expect(isOffsetInCodeBlock('```js', 5)).toBe(true); + }); + + it('is true inside a still-open block while it is being typed', () => { + const open = '```js\nconst a = 1;'; + expect(isOffsetInCodeBlock(open, open.length)).toBe(true); + }); + + it('is true inside a closed block and false outside it', () => { + expect(isOffsetInCodeBlock(BLOCK, 6)).toBe(true); + expect(isOffsetInCodeBlock(BLOCK, 0)).toBe(false); + expect(isOffsetInCodeBlock(BLOCK, BLOCK.length)).toBe(false); + expect(isOffsetInCodeBlock(BLOCK + '\nafter', BLOCK.length + 5)).toBe(false); + }); + + it('toggles per fence across multiple blocks', () => { + const two = BLOCK + '\ntext\n' + BLOCK; + const secondBlock = two.lastIndexOf(BLOCK); + expect(isOffsetInCodeBlock(two, secondBlock - 2)).toBe(false); + expect(isOffsetInCodeBlock(two, secondBlock + 6)).toBe(true); + expect(isOffsetInCodeBlock(two, two.length)).toBe(false); + }); +}); diff --git a/tools/ui/tests/unit/contenteditable-word-jump.test.ts b/tools/ui/tests/unit/contenteditable-word-jump.test.ts new file mode 100644 index 000000000..8e68950d4 --- /dev/null +++ b/tools/ui/tests/unit/contenteditable-word-jump.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest'; +import { badgeAwareWordJump, leadingBadgeEdgeOffset } from '$lib/utils'; + +// Layout of `hello [docs](file:///a/b) world foo`: +// "hello" 0-4, " " 5, badge 6-24 (length 19), " " 25, "world" 26-30, " " 31, "foo" 32-34 +const BADGE = '[docs](file:///a/b)'; +const SOURCE = `hello ${BADGE} world foo`; +const BADGE_START = 6; +const BADGE_END = 25; + +describe('badgeAwareWordJump', () => { + it('returns null when the buffer has no badge', () => { + expect(badgeAwareWordJump('hello world', 0, 'forward')).toBeNull(); + expect(badgeAwareWordJump('hello world', 11, 'backward')).toBeNull(); + }); + + it('jumps forward onto a badge landing at its end, not the next word', () => { + expect(badgeAwareWordJump(SOURCE, BADGE_START, 'forward')).toBe(BADGE_END); + }); + + it('jumps forward from the space before a badge landing at its end', () => { + expect(badgeAwareWordJump(SOURCE, BADGE_START - 1, 'forward')).toBe(BADGE_END); + }); + + it('jumps backward over a badge landing at its start', () => { + expect(badgeAwareWordJump(SOURCE, BADGE_END, 'backward')).toBe(BADGE_START); + }); + + it('jumps backward from the next word onto the badge start', () => { + // caret at the start of "world" + expect(badgeAwareWordJump(SOURCE, BADGE_END + 1, 'backward')).toBe(BADGE_START); + }); + + it('returns null for jumps that cross no badge', () => { + // forward over "hello" only + expect(badgeAwareWordJump(SOURCE, 0, 'forward')).toBeNull(); + // backward over "foo" only + expect(badgeAwareWordJump(SOURCE, SOURCE.length, 'backward')).toBeNull(); + // backward away from the badge (over "hello") + expect(badgeAwareWordJump(SOURCE, BADGE_START, 'backward')).toBeNull(); + }); + + it('treats a leading badge as one word in both directions', () => { + const source = `${BADGE} rest`; + expect(badgeAwareWordJump(source, 0, 'forward')).toBe(BADGE.length); + expect(badgeAwareWordJump(source, BADGE.length, 'backward')).toBe(0); + }); + + it('treats adjacent badges as separate words', () => { + // each badge is 14 chars: "[a](file:///x)" / "[b](file:///y)" + const source = '[a](file:///x)[b](file:///y)'; + expect(badgeAwareWordJump(source, 0, 'forward')).toBe(14); + expect(badgeAwareWordJump(source, 14, 'forward')).toBe(28); + expect(badgeAwareWordJump(source, 28, 'backward')).toBe(14); + expect(badgeAwareWordJump(source, 14, 'backward')).toBe(0); + }); + + it('jumps over a badge following punctuation', () => { + // "foo," 0-3, " " 4, badge 5-23 (end 24), " bar" 24-27 + const source = `foo, ${BADGE} bar`; + expect(badgeAwareWordJump(source, 0, 'forward')).toBeNull(); + expect(badgeAwareWordJump(source, 3, 'forward')).toBe(24); + }); +}); + +describe('leadingBadgeEdgeOffset', () => { + it('returns 0 when the caret sits exactly at a leading badge end', () => { + expect(leadingBadgeEdgeOffset(`${BADGE} rest`, BADGE.length)).toBe(0); + }); + + it('returns null when the caret is anywhere else', () => { + expect(leadingBadgeEdgeOffset(`${BADGE} rest`, 0)).toBeNull(); + expect(leadingBadgeEdgeOffset(`${BADGE} rest`, BADGE.length + 2)).toBeNull(); + }); + + it('returns null when the buffer does not start with a badge', () => { + expect(leadingBadgeEdgeOffset(SOURCE, BADGE_END)).toBeNull(); + expect(leadingBadgeEdgeOffset('', 0)).toBeNull(); + }); +}); diff --git a/tools/ui/tests/unit/glob-search-children.test.ts b/tools/ui/tests/unit/glob-search-children.test.ts new file mode 100644 index 000000000..46b7caa4c --- /dev/null +++ b/tools/ui/tests/unit/glob-search-children.test.ts @@ -0,0 +1,119 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('$lib/services/tools.service', () => ({ + ToolsService: { executeToolRaw: vi.fn() } +})); + +import { ToolsService } from '$lib/services/tools.service'; +import { GlobSearchType } from '$lib/enums'; +import { runGlobSearchWithChildren } from '$lib/utils'; + +const mockExecute = vi.mocked(ToolsService.executeToolRaw); + +// Distinct roots per test so the module-level search cache never serves a +// prior test's result under the same (type, path, glob, depth) key. +beforeEach(() => { + mockExecute.mockReset(); +}); + +describe('runGlobSearchWithChildren', () => { + it('returns ranked outer entries as absolute paths without descending', async () => { + mockExecute.mockResolvedValueOnce({ + base: '/Users/rootA', + entries: [ + { path: 'note.md', type: 'file' }, + { path: 'src', type: 'dir' } + ] + }); + const res = await runGlobSearchWithChildren( + 'note', + '/Users/rootA', + 3, + 50, + new AbortController().signal + ); + expect(res.error).toBeUndefined(); + expect(res.entries.map((e) => e.path)).toEqual(['/Users/rootA/note.md', '/Users/rootA/src']); + expect(res.exactDir).toBeUndefined(); + expect(mockExecute).toHaveBeenCalledTimes(1); + }); + + it('appends a matched directorys children when the query ends with a separator', async () => { + mockExecute + .mockResolvedValueOnce({ base: '/Users/rootB', entries: [{ path: 'src', type: 'dir' }] }) + .mockResolvedValueOnce({ + base: '/Users/rootB/src', + entries: [ + { path: 'a.txt', type: 'file' }, + { path: 'sub', type: 'dir' } + ] + }); + const res = await runGlobSearchWithChildren( + '/Users/rootB/src/', + '/Users/rootB', + 3, + 50, + new AbortController().signal, + { type: GlobSearchType.ALL, descendOnTrailingSeparator: true } + ); + expect(res.error).toBeUndefined(); + expect(res.exactDir).toBe('/Users/rootB/src'); + expect(res.entries.map((e) => e.path)).toEqual([ + '/Users/rootB/src', + '/Users/rootB/src/a.txt', + '/Users/rootB/src/sub' + ]); + expect(mockExecute).toHaveBeenCalledTimes(2); + }); + + it('does not descend without a trailing separator in mention mode', async () => { + mockExecute.mockResolvedValueOnce({ + base: '/Users/rootC', + entries: [{ path: 'src', type: 'dir' }] + }); + const res = await runGlobSearchWithChildren( + '/Users/rootC/src', + '/Users/rootC', + 3, + 50, + new AbortController().signal, + { type: GlobSearchType.ALL, descendOnTrailingSeparator: true } + ); + expect(res.exactDir).toBeUndefined(); + expect(mockExecute).toHaveBeenCalledTimes(1); + }); + + it('descends on an exact directory match in WD mode', async () => { + mockExecute + .mockResolvedValueOnce({ base: '/Users/rootD', entries: [{ path: 'src', type: 'dir' }] }) + .mockResolvedValueOnce({ + base: '/Users/rootD/src', + entries: [{ path: 'a.txt', type: 'file' }] + }); + const res = await runGlobSearchWithChildren( + '/Users/rootD/src', + '/Users/rootD', + 3, + 50, + new AbortController().signal, + { type: GlobSearchType.DIR } + ); + expect(res.exactDir).toBe('/Users/rootD/src'); + expect(res.entries.map((e) => e.path)).toEqual(['/Users/rootD/src', '/Users/rootD/src/a.txt']); + expect(mockExecute).toHaveBeenCalledTimes(2); + }); + + it('surfaces a server error without attempting a child walk', async () => { + mockExecute.mockResolvedValueOnce({ error: 'boom' }); + const res = await runGlobSearchWithChildren( + 'src', + '/Users/rootE', + 3, + 50, + new AbortController().signal + ); + expect(res.error).toBe('boom'); + expect(res.entries).toEqual([]); + expect(mockExecute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tools/ui/tests/unit/mention-badge.test.ts b/tools/ui/tests/unit/mention-badge.test.ts new file mode 100644 index 000000000..f2c004182 --- /dev/null +++ b/tools/ui/tests/unit/mention-badge.test.ts @@ -0,0 +1,174 @@ +import { describe, expect, it } from 'vitest'; +import { + MENTION_BADGE_FILE_ICON_PATHS, + MENTION_BADGE_FOLDER_ICON_PATHS, + buildMentionInsertion, + containsFileMentionLink, + decodeFileLinkPath, + encodeFileLinkPath, + fileMentionLinkRe, + getMentionBadgeIconPaths, + getMentionBadgeLabel +} from '$lib/utils'; +import { FileMentionEntryType } from '$lib/enums'; + +describe('encodeFileLinkPath', () => { + it('leaves a clean path unchanged', () => { + expect(encodeFileLinkPath('/Users/foo/bar.txt')).toBe('/Users/foo/bar.txt'); + }); + + it('encodes spaces per path segment', () => { + expect( + encodeFileLinkPath('/Users/allozaur/Desktop/Screenshot 2026-08-05 at 11.33.45.png') + ).toBe('/Users/allozaur/Desktop/Screenshot%202026-08-05%20at%2011.33.45.png'); + }); + + it('preserves the leading and trailing slash (directory marker)', () => { + expect(encodeFileLinkPath('/Users/foo/bar/')).toBe('/Users/foo/bar/'); + }); + + it('encodes parentheses in macOS screenshot names', () => { + expect(encodeFileLinkPath('/Users/foo/Pic (1).png')).toBe('/Users/foo/Pic%20(1).png'); + }); +}); + +describe('fileMentionLinkRe', () => { + it('matches a standard mention link', () => { + expect(fileMentionLinkRe().test('[docs](file:///a/b)')).toBe(true); + expect(containsFileMentionLink('[docs](file:///a/b)')).toBe(true); + }); + + it('does not match non-file links', () => { + expect(fileMentionLinkRe().test('[foo](https://example.com)')).toBe(false); + expect(fileMentionLinkRe().test('plain text')).toBe(false); + }); + + it('admits a close paren in a macOS-style file name', () => { + const match = fileMentionLinkRe().exec( + '[Screenshot (1).png](file:///Users/foo/Screenshot (1).png)' + ); + expect(match).not.toBeNull(); + expect(match?.[1]).toBe('Screenshot (1).png'); + expect(match?.[2]).toBe('/Users/foo/Screenshot (1).png'); + }); + + it('admits a parenthesized folder segment', () => { + expect( + fileMentionLinkRe().exec('[main.rs](file:///Users/foo/Project (Stuff)/main.rs)')?.[2] + ).toBe('/Users/foo/Project (Stuff)/main.rs'); + }); + + it('stops at the closing paren of an adjacent badge', () => { + expect(fileMentionLinkRe().exec('[a](file:///p)[b](file:///q)')?.[0]).toBe('[a](file:///p)'); + }); +}); + +describe('getMentionBadgeIconPaths', () => { + it('returns the folder glyphs for a trailing-separator path', () => { + expect(getMentionBadgeIconPaths('/Users/foo/bar/')).toBe(MENTION_BADGE_FOLDER_ICON_PATHS); + }); + + it('returns the file glyphs otherwise', () => { + expect(getMentionBadgeIconPaths('/Users/foo/bar.txt')).toBe(MENTION_BADGE_FILE_ICON_PATHS); + }); +}); + +describe('getMentionBadgeLabel', () => { + it('returns the name by default', () => { + expect(getMentionBadgeLabel('bar', '/Users/foo/bar/', false)).toBe('bar'); + }); + + it('renders the decoded full path without the trailing separator', () => { + expect(getMentionBadgeLabel('bar', '/Users/foo/bar/', true)).toBe('/Users/foo/bar'); + expect(getMentionBadgeLabel('shot', '/Users/foo/Screenshot%20(1).png', true)).toBe( + '/Users/foo/Screenshot (1).png' + ); + }); + + it('abbreviates a known home prefix to a tilde', () => { + expect(getMentionBadgeLabel('main.rs', '/home/user/src/main.rs', true, '/home/user')).toBe( + '~/src/main.rs' + ); + }); + + it('falls back to the name when the decoded path is empty', () => { + expect(getMentionBadgeLabel('root', '/', true)).toBe('root'); + }); +}); + +describe('decodeFileLinkPath', () => { + it('decodes encoded segments back to the original path', () => { + expect( + decodeFileLinkPath('/Users/allozaur/Desktop/Screenshot%202026-08-05%20at%2011.33.45.png') + ).toBe('/Users/allozaur/Desktop/Screenshot 2026-08-05 at 11.33.45.png'); + }); + + it('is the inverse of encodeFileLinkPath', () => { + for (const path of [ + '/a/b.txt', + '/Users/foo/Desktop/Screenshot 2026-08-05 at 11.33.45.png', + '/Users/foo/bar (1)/dir/', + '/sp ace/pa%th.txt' + ]) { + expect(decodeFileLinkPath(encodeFileLinkPath(path))).toBe(path); + } + }); + + it('falls back to the input on malformed percent sequences', () => { + expect(decodeFileLinkPath('/a/%zz.txt')).toBe('/a/%zz.txt'); + }); +}); + +describe('buildMentionInsertion', () => { + const file = (path: string, name: string) => ({ + path, + name, + type: FileMentionEntryType.FILE + }); + const dir = (path: string, name: string) => ({ + path, + name, + type: FileMentionEntryType.DIRECTORY + }); + + it('splices a root-anchored file link in place of the token', () => { + const value = 'hello @repo'; + const result = buildMentionInsertion(file('/Users/foo/myRepo', 'myRepo'), value, { + start: 6, + end: 11 + }); + expect(result).not.toBeNull(); + const { newValue, caretOffset } = result!; + expect(newValue).toBe('hello [myRepo](file:///Users/foo/myRepo) '); + expect(caretOffset).toBe(6 + '[myRepo](file:///Users/foo/myRepo) '.length); + }); + + it('keeps the trailing slash on the directory marker', () => { + const value = 'see @src'; + const { newValue } = buildMentionInsertion(dir('/Users/foo/myRepo/src/', 'src'), value, { + start: 4, + end: 8 + })!; + expect(newValue).toBe('see [src](file:///Users/foo/myRepo/src/) '); + }); + + it('escapes spaces and parens in the target', () => { + const value = '@pic'; + const { newValue } = buildMentionInsertion( + file('/Users/foo/Desktop/Pic (1).png', 'Pic (1).png'), + value, + { start: 0, end: 4 } + )!; + expect(newValue).toBe('[Pic (1).png](file:///Users/foo/Desktop/Pic%20(1).png) '); + }); + + it('re-adds the directory marker when the cleaned path empties', () => { + const { newValue } = buildMentionInsertion(dir('/', 'root'), '/', { start: 0, end: 1 })!; + expect(newValue).toBe('[root](file:///) '); + }); + + it('returns null for an out-of-range token', () => { + expect(buildMentionInsertion(file('/a/b.txt', 'b.txt'), 'x', { start: 0, end: 5 })).toBeNull(); + expect(buildMentionInsertion(file('/a/b.txt', 'b.txt'), 'x', { start: 2, end: 1 })).toBeNull(); + }); +}); diff --git a/tools/ui/tests/unit/mention-token.test.ts b/tools/ui/tests/unit/mention-token.test.ts new file mode 100644 index 000000000..fd21a94db --- /dev/null +++ b/tools/ui/tests/unit/mention-token.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest'; +import { findMentionToken, takeMentionDismissSnapshot } from '$lib/utils'; + +describe('findMentionToken', () => { + it('returns null for an empty/bare cursor', () => { + expect(findMentionToken('', 0)).toBeNull(); + expect(findMentionToken('text', 0)).toBeNull(); + }); + + it('recognizes a mention at the start of the value', () => { + expect(findMentionToken('@pr', 3)).toEqual({ start: 0, end: 3, query: 'pr' }); + }); + + it('recognizes a mention after a word boundary', () => { + expect(findMentionToken('hello @pr', 9)).toEqual({ start: 6, end: 9, query: 'pr' }); + }); + + it('returns null when the @ is mid-identifier', () => { + expect(findMentionToken('em@', 3)).toBeNull(); + expect(findMentionToken('text@pr', 7)).toBeNull(); + }); + + it('returns null when the cursor is past the whitespace break', () => { + expect(findMentionToken('@pr hello', 9)).toBeNull(); + }); + + it('treats boundary characters (parens, brackets, comma) as token starts', () => { + expect(findMentionToken('(@pr', 4)).toEqual({ start: 1, end: 4, query: 'pr' }); + expect(findMentionToken('[@pr', 4)).toEqual({ start: 1, end: 4, query: 'pr' }); + expect(findMentionToken('a,@pr', 5)).toEqual({ start: 2, end: 5, query: 'pr' }); + }); + + it('does not treat an identifier character as a boundary', () => { + expect(findMentionToken('user@abc', 8)).toBeNull(); + }); + + it('extracts the whole token up to the trailing boundary as the query', () => { + expect(findMentionToken('@', 1)).toEqual({ start: 0, end: 1, query: '' }); + expect(findMentionToken('@hello', 6)).toEqual({ start: 0, end: 6, query: 'hello' }); + }); + + it('keeps the whole token as the query when the caret is mid-token', () => { + expect(findMentionToken('@hello', 4)).toEqual({ start: 0, end: 6, query: 'hello' }); + expect(findMentionToken('@hello world', 4)).toEqual({ start: 0, end: 6, query: 'hello' }); + }); + + it('ignores a boundary @ and keeps the most recent token', () => { + expect(findMentionToken('a @foo @bar', 11)).toEqual({ start: 7, end: 11, query: 'bar' }); + }); +}); + +describe('takeMentionDismissSnapshot', () => { + it('returns null when there is no valid mention at the cursor', () => { + expect(takeMentionDismissSnapshot('plain text', 5)).toBeNull(); + expect(takeMentionDismissSnapshot('user@abc', 8)).toBeNull(); + }); + + it('captures start and query of the current mention', () => { + expect(takeMentionDismissSnapshot('hello @proj', 11)).toEqual({ + start: 6, + query: 'proj' + }); + }); +}); diff --git a/tools/ui/tests/unit/source-history.test.ts b/tools/ui/tests/unit/source-history.test.ts new file mode 100644 index 000000000..b83030941 --- /dev/null +++ b/tools/ui/tests/unit/source-history.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest'; +import { SourceHistory } from '$lib/utils'; + +describe('SourceHistory', () => { + it('coalesces pushes inside the group window into one undo step', () => { + const h = new SourceHistory(100, 800); + h.push({ value: '', caret: 0 }, 1000); + h.push({ value: 'a', caret: 1 }, 1200); + h.push({ value: 'ab', caret: 2 }, 1500); + + expect(h.undo({ value: 'abc', caret: 3 })).toEqual({ value: '', caret: 0 }); + expect(h.undo({ value: '', caret: 0 })).toBeNull(); + }); + + it('starts a new group once the window has passed', () => { + const h = new SourceHistory(100, 800); + h.push({ value: '', caret: 0 }, 1000); + h.push({ value: 'abc', caret: 3 }, 2000); + + expect(h.undo({ value: 'abcdef', caret: 6 })).toEqual({ value: 'abc', caret: 3 }); + expect(h.undo({ value: 'abc', caret: 3 })).toEqual({ value: '', caret: 0 }); + }); + + it('newGroup forces a separate entry even inside the window', () => { + const h = new SourceHistory(100, 800); + h.push({ value: '', caret: 0 }, 1000); + h.push({ value: 'abc', caret: 3 }, 1100, true); + + expect(h.undo({ value: 'abc\n', caret: 4 })).toEqual({ value: 'abc', caret: 3 }); + expect(h.undo({ value: 'abc', caret: 3 })).toEqual({ value: '', caret: 0 }); + }); + + it('redo round-trips and a fresh push clears the redo stack', () => { + const h = new SourceHistory(100, 800); + h.push({ value: '', caret: 0 }, 1000); + + const undone = h.undo({ value: 'abc', caret: 3 }); + expect(undone).toEqual({ value: '', caret: 0 }); + expect(h.redo({ value: '', caret: 0 })).toEqual({ value: 'abc', caret: 3 }); + + h.undo({ value: 'abc', caret: 3 }); + h.push({ value: '', caret: 0 }, 5000); + expect(h.redo({ value: 'x', caret: 1 })).toBeNull(); + }); + + it('starts a new group on the first edit after an undo', () => { + const h = new SourceHistory(100, 800); + h.push({ value: '', caret: 0 }, 1000); + h.undo({ value: 'abc', caret: 3 }); + + h.push({ value: '', caret: 0 }, 1200); + expect(h.undo({ value: 'x', caret: 1 })).toEqual({ value: '', caret: 0 }); + }); + + it('evicts the oldest entry past the limit', () => { + const h = new SourceHistory(2, 800); + h.push({ value: 'one', caret: 0 }, 1000); + h.push({ value: 'two', caret: 0 }, 2000); + h.push({ value: 'three', caret: 0 }, 3000); + + expect(h.undo({ value: 'cur', caret: 0 })).toEqual({ value: 'three', caret: 0 }); + expect(h.undo({ value: 'three', caret: 0 })).toEqual({ value: 'two', caret: 0 }); + expect(h.undo({ value: 'two', caret: 0 })).toBeNull(); + }); +}); diff --git a/tools/ui/tests/unit/working-directory.test.ts b/tools/ui/tests/unit/working-directory.test.ts index f7858e29b..7481c154d 100644 --- a/tools/ui/tests/unit/working-directory.test.ts +++ b/tools/ui/tests/unit/working-directory.test.ts @@ -2,10 +2,12 @@ import { describe, expect, it } from 'vitest'; import { splitPathQuery, buildCaseInsensitiveGlob, + buildGlobSearchArgs, rankEntries, joinPath, highlightMatch } from '$lib/utils'; +import { GLOB_WILDCARD, PATH_NAV_MAX_DEPTH } from '$lib/constants'; describe('splitPathQuery', () => { it('treats a plain query as a home-relative glob (not navigation)', () => { @@ -124,3 +126,41 @@ describe('highlightMatch', () => { expect(highlightMatch('abc', 'z')).toEqual([{ text: 'abc', match: false }]); }); }); + +describe('buildGlobSearchArgs', () => { + const DEPTH = 6; + + it('glob-matches home-relative within the scope path', () => { + const args = buildGlobSearchArgs('docs', '/home', DEPTH); + expect(args.path).toBe('/home'); + expect(args.include).toBe(buildCaseInsensitiveGlob('docs')); + expect(args.maxDepth).toBe(DEPTH); + expect(args.rankQuery).toBe('docs'); + expect(args.last).toBeUndefined(); + }); + + it('navigates home for a `~` path query', () => { + const args = buildGlobSearchArgs('~/proj', '/home', DEPTH); + expect(args.path).toBe('~'); + expect(args.include).toBe(buildCaseInsensitiveGlob('proj')); + expect(args.maxDepth).toBe(PATH_NAV_MAX_DEPTH); + expect(args.rankQuery).toBe('proj'); + expect(args.last).toBe('proj'); + }); + + it('lists the scope root when a path query has no last segment', () => { + const args = buildGlobSearchArgs('~/', '/home', DEPTH); + expect(args.path).toBe('~'); + expect(args.include).toBe(GLOB_WILDCARD); + expect(args.maxDepth).toBe(PATH_NAV_MAX_DEPTH); + }); + + it('navigates an absolute path under its root', () => { + const args = buildGlobSearchArgs('/usr/local/bin', '/home', DEPTH); + expect(args.path).toBe('/usr/local'); + expect(args.include).toBe(buildCaseInsensitiveGlob('bin')); + expect(args.maxDepth).toBe(PATH_NAV_MAX_DEPTH); + expect(args.rankQuery).toBe('bin'); + expect(args.last).toBe('bin'); + }); +});