opencode-session-turns 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,11 +22,13 @@ click a turn to jump straight to that message.
22
22
  - 列出当前会话的每一轮用户提问(从会话消息接口分页拉取,`type=user`)。
23
23
  - 鼠标移到窗口右缘的细条上自动展开,移开自动收起;长历史跳转期间面板保持展开。
24
24
  - 点击任意一轮,**跳转到对应消息**——包括很久以前、尚未渲染的历史轮次。
25
- - 深历史需要逐页加载时,面板头部显示「加载历史中 · N 页 · Xs」,目标轮次带「加载中」
26
- 徽标并琥珀色脉冲;按 `Esc` 或点 × 可随时取消。
25
+ - 深历史需要逐页加载时,面板头部显示进度,目标轮次带徽标并琥珀色脉冲;
26
+ 按 `Esc` 或点 × 可随时取消。
27
27
  - **历史预热**:宿主自己加载历史每页之间有 2 秒硬冷却,深历史跳转因此要等好几秒。
28
- 轮次条会在你打开面板时(悬停或 `/turns`)后台把历史预热完并还原视口,
29
- 之后所有跳转都是瞬时的;也可以点面板上的「预热」手动触发。
28
+ 轮次条会在**打开会话时**(轮次列表加载完、≥15 轮)就后台把历史预热完并还原视口,
29
+ 悬停面板或 `/turns` 也会触发。预热完成后所有跳转都是瞬时的。
30
+ - **点击与预热的配合**:点到的轮次已预热 → 直接瞬时跳转;还没预热到 → 头部显示
31
+ 「预热中 · 稍等一下」,等预热翻到该轮后自动落地(不打断预热)。
30
32
  - 定位成功后精准描边闪烁**那一条用户气泡**(跟随气泡自身圆角),不再整行填色。
31
33
  - 自动高亮当前视口所在的轮次。
32
34
  - 斜杠命令 `/turns`、快捷键 `⌘⌥R`(Windows/Linux 为 `Ctrl+Alt+R`)展开轮次条。
@@ -1,5 +1,5 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const o=require("solid-js/web"),q=require("@opencode/plugin/gui"),l=require("solid-js");var Le=o.template("<div class=sr-status>"),Xe=o.template('<span class="sr-title sr-progress">加载历史中'),Qe=o.template("<div class=sr-foot>"),et=o.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),tt=o.template("<span class=sr-title>会话轮次"),rt=o.template("<div class=sr-note>"),nt=o.template("<span class=sr-badge>加载中"),st=o.template("<button type=button class=sr-turn><span class=sr-text><span class=sr-num>#</span><span class=sr-time></span></span><span class=sr-dash>");const De="v16",le="message-",Pe="session-turns:diag",ce="session-turns:open",ot=40,at=200,it=50,lt=30,ct=45e3,dt=2e4,ut=8e3,pt=12e3,ft=220,gt=800;let Be=0,de="",ue="",pe="";const je=a=>{const s=String(a?.text??"").split(`
2
- `).map(p=>p.trim()).find(p=>p.length>0);if(s)return s;const d=a?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},Re=a=>String(a).padStart(2,"0"),ht=a=>{const s=new Date(a??0);return`${Re(s.getHours())}:${Re(s.getMinutes())}`},se=a=>new Promise(s=>setTimeout(s,a)),fe=()=>document.querySelector('[data-slot="session-timeline-scroll"] > .scroll-view__viewport')??document.querySelector('[data-slot="session-timeline-scroll"] [data-scrollable]')??(()=>{let s=document.querySelector("[data-message-id]")?.parentElement;for(;s;){const d=getComputedStyle(s);if(/(auto|scroll)/.test(d.overflowY)&&s.scrollHeight>s.clientHeight+1)return s;s=s.parentElement}})(),y=a=>{const s=document.getElementById(`${le}${a}`);if(s)return s;const d=`[data-message-id="${a.replace(/"/g,'\\"')}"]`;for(const p of document.querySelectorAll(d))if(p.getAttribute("data-timeline-row")==="UserMessage"||p.querySelector('[data-slot="user-message-text"]'))return p},oe=a=>{const s=fe(),d=[...document.querySelectorAll("[data-message-id]")],p=d[0]?.getAttribute("data-message-id")??"",S=pe?` · 目标 ${y(pe)?"在":"不在"} DOM`:"",$=p?` · 行ID ${p.slice(0,20)}`:"";let E="(空)";try{E=a.router.path()||"(空)"}catch{}return`轮次 ${Be} · 容器 ${s?"OK":"缺失"} · DOM ${d.length} 行${S}${$} · 路由 ${E} · hash ${window.location.hash||"(空)"}${ue?` · ${ue}`:""}${de?` · ${de}`:""}`},mt=a=>{const s=a.getBoundingClientRect().top;let d=s;for(const p of a.querySelectorAll(".sticky, [data-session-title]")){if(!(p instanceof HTMLElement)||getComputedStyle(p).position!=="sticky"&&!p.hasAttribute("data-session-title"))continue;const $=p.getBoundingClientRect();$.height!==0&&($.top>s+4||(d=Math.max(d,$.bottom)))}return d},ae=a=>{try{const s=fe();if(!s||!a.isConnected)return;const d=a.querySelector('[data-slot="user-message-text"]')??a,p=mt(s),S=d.getBoundingClientRect().top-(p+18);Math.abs(S)>2&&(s.scrollTop+=S),ue=`inset ${Math.round(p)} · delta ${Math.round(S)}`}catch{}},ie=a=>{ae(a),setTimeout(()=>ae(a),450),setTimeout(()=>ae(a),1e3)},qe=a=>{let s="";try{s=a.router.path()||""}catch{}return s.startsWith("/")?s:""},bt=a=>{const s=document.createElement("a");s.setAttribute("href",a),s.setAttribute("replace",""),s.setAttribute("noscroll",""),s.style.display="none",document.body.appendChild(s);try{s.dispatchEvent(new MouseEvent("click",{bubbles:!0,cancelable:!0,button:0,view:window}))}finally{s.remove()}},wt=`
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const s=require("solid-js/web"),H=require("@opencode/plugin/gui"),a=require("solid-js");var qe=s.template("<div class=sr-status>"),tt=s.template('<span class="sr-title sr-progress">'),rt=s.template("<div class=sr-foot>"),nt=s.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),st=s.template("<span class=sr-title>会话轮次"),ot=s.template("<div class=sr-note>"),at=s.template("<span class=sr-badge>"),it=s.template("<button type=button class=sr-turn><span class=sr-text><span class=sr-num>#</span><span class=sr-time></span></span><span class=sr-dash>");const Be="v19",ue="message-",Je="session-turns:diag",pe="session-turns:open",lt=40,ct=200,dt=50,ut=30,pt=45e3,ft=2e4,gt=8e3,ht=12e3,mt=220,bt=800;let He=0,fe="",ge="",he="";const Oe=l=>{const o=String(l?.text??"").split(`
2
+ `).map(p=>p.trim()).find(p=>p.length>0);if(o)return o;const d=l?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},Ne=l=>String(l).padStart(2,"0"),wt=l=>{const o=new Date(l??0);return`${Ne(o.getHours())}:${Ne(o.getMinutes())}`},le=l=>new Promise(o=>setTimeout(o,l)),me=()=>document.querySelector('[data-slot="session-timeline-scroll"] > .scroll-view__viewport')??document.querySelector('[data-slot="session-timeline-scroll"] [data-scrollable]')??(()=>{let o=document.querySelector("[data-message-id]")?.parentElement;for(;o;){const d=getComputedStyle(o);if(/(auto|scroll)/.test(d.overflowY)&&o.scrollHeight>o.clientHeight+1)return o;o=o.parentElement}})(),k=l=>{const o=document.getElementById(`${ue}${l}`);if(o)return o;const d=`[data-message-id="${l.replace(/"/g,'\\"')}"]`;for(const p of document.querySelectorAll(d))if(p.getAttribute("data-timeline-row")==="UserMessage"||p.querySelector('[data-slot="user-message-text"]'))return p},ce=l=>{const o=me(),d=[...document.querySelectorAll("[data-message-id]")],p=d[0]?.getAttribute("data-message-id")??"",E=he?` · 目标 ${k(he)?"在":"不在"} DOM`:"",_=p?` · 行ID ${p.slice(0,20)}`:"";let C="(空)";try{C=l.router.path()||"(空)"}catch{}return`轮次 ${He} · 容器 ${o?"OK":"缺失"} · DOM ${d.length} 行${E}${_} · 路由 ${C} · hash ${window.location.hash||"(空)"}${ge?` · ${ge}`:""}${fe?` · ${fe}`:""}`},xt=l=>{const o=l.getBoundingClientRect().top;let d=o;for(const p of l.querySelectorAll(".sticky, [data-session-title]")){if(!(p instanceof HTMLElement)||getComputedStyle(p).position!=="sticky"&&!p.hasAttribute("data-session-title"))continue;const _=p.getBoundingClientRect();_.height!==0&&(_.top>o+4||(d=Math.max(d,_.bottom)))}return d},de=l=>{try{const o=me();if(!o||!l.isConnected)return;const d=l.querySelector('[data-slot="user-message-text"]')??l,p=xt(o),E=d.getBoundingClientRect().top-(p+18);Math.abs(E)>2&&(o.scrollTop+=E),ge=`inset ${Math.round(p)} · delta ${Math.round(E)}`}catch{}},te=l=>{de(l),setTimeout(()=>de(l),450),setTimeout(()=>de(l),1e3)},ze=l=>{let o="";try{o=l.router.path()||""}catch{}return o.startsWith("/")?o:""},vt=l=>{const o=document.createElement("a");o.setAttribute("href",l),o.setAttribute("replace",""),o.setAttribute("noscroll",""),o.style.display="none",document.body.appendChild(o);try{o.dispatchEvent(new MouseEvent("click",{bubbles:!0,cancelable:!0,button:0,view:window}))}finally{o.remove()}},yt=`
3
3
  .sr-rail {
4
4
  position: fixed;
5
5
  top: 50%;
@@ -242,4 +242,4 @@
242
242
  white-space: pre-wrap;
243
243
  max-width: 260px;
244
244
  }
245
- `;function xt(a){const s=q.usePlugin(),[d,p]=l.createSignal([]),[S,$]=l.createSignal(""),[E,I]=l.createSignal(!1),[Je,Oe]=l.createSignal(""),[L,ge]=l.createSignal(""),[x,B]=l.createSignal(!1),[he,D]=l.createSignal(0),[me,P]=l.createSignal(0),[yt,J]=l.createSignal(0),[O,N]=l.createSignal(""),_=new Map;let W=0,H=!1,z=!1,V="",j;const v=()=>a.session()?.id,Y=()=>{const e=[..._.values()].sort((r,i)=>r.created-i.created),t=l.untrack(d);t.length===e.length&&t.every((r,i)=>r===e[i])||(p(e),Be=e.length)},be=e=>{const t=_.get(e.id);t&&t.created===e.time&&t.text===e.text||_.set(e.id,{id:e.id,created:e.time,text:e.text})},Ne=async(e,t,r)=>{const i=r?{sessionID:e,type:"user",limit:t,cursor:r}:{sessionID:e,type:"user",limit:t,order:"desc"};return await s.client.message.list(i)},K=async(e,t)=>{const r=++W;H=!0;let i="";try{let n;const c=t==="full"?ot:1,u=t==="full"?at:it;let g=t!=="full";for(let b=0;b<c;b++){let m;try{m=await Ne(e,u,n)}catch(h){i=String(h?.message??h);break}if(r!==W)return;g||(_.clear(),g=!0);let w=0;for(const h of m?.data??[])_.has(h.id)||(be({id:h.id,time:h.time?.created??0,text:je(h)}),w++);Y();const f=m?.cursor?.next;if(!f||w===0)break;n=f}}finally{H=!1,V=e,de=i,Oe(_.size===0?i||"暂无用户消息":""),z&&(z=!1,we())}},we=()=>{j||(j=setTimeout(()=>{j=void 0;const e=l.untrack(v);e&&K(e,"light")},300))},xe=e=>{if(!(!e||e!==l.untrack(v))){if(H){z=!0;return}we()}},He=e=>{const t=e?.type;if(t==="message.updated"){const r=e?.data??{},i=r.info;i?.role==="user"&&i?.id&&(be({id:i.id,time:i.time?.created??0,text:je(i)}),Y()),xe(r.sessionID)}else if(t==="message.removed"){const r=e?.data??{};r.messageID&&_.delete(r.messageID)&&Y(),xe(r.sessionID)}else if(t==="location.shutdown"){const r=l.untrack(v);r&&V===r&&K(r,"full")}};l.onMount(()=>{let e=!1,t=0,r;(async()=>{for(;!e;){const c=s.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!e;){const u=await c.next();if(u.done)break;t=0,He(u.value)}}catch{}try{await c.return?.()}catch{}if(e)break;t++,await se(Math.min(2e3*t,15e3))}})(),l.onCleanup(()=>{e=!0,clearTimeout(r),j&&clearTimeout(j)})}),l.createEffect(()=>{const e=v();e&&l.untrack(()=>{V!==e&&(W++,H=!1,z=!1,_.clear(),p([]),$(""),X="",K(e,"full"))})});let F;const C=()=>(F?.isConnected||(F=fe()),F),ze=()=>{const e=C();if(!e)return;const t=e.querySelectorAll("[data-message-id]");if(t.length===0)return;const r=e.getBoundingClientRect().top+40;for(const i of t)if(i.getBoundingClientRect().bottom>=r){const n=i.getAttribute("data-message-id");n&&$(n);break}};let Z=!1;const ve=()=>{Z||(Z=!0,requestAnimationFrame(()=>{Z=!1,ze()}))};l.onMount(()=>{const e=()=>ve();document.addEventListener("scroll",e,!0);const t=setInterval(ve,1500);l.onCleanup(()=>{document.removeEventListener("scroll",e,!0),clearInterval(t)})});let ye=0;const U=(e,t=2600)=>{const r=++ye;ge(e),setTimeout(()=>{r===ye&&ge("")},t)};l.onMount(()=>{const e=r=>{if(r.key==="Escape"){if(l.untrack(x)){Ee(),r.stopPropagation();return}l.untrack(E)&&(I(!1),r.stopPropagation())}},t=r=>{if(!l.untrack(E))return;(r.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||I(!1)};window.addEventListener("keydown",e,!0),document.addEventListener("pointerdown",t,!0),l.onCleanup(()=>{window.removeEventListener("keydown",e,!0),document.removeEventListener("pointerdown",t,!0)})}),l.onMount(()=>{const e=()=>U(`${De} · ${oe(s)}`),t=()=>{I(r=>!r),Ce()};window.addEventListener(Pe,e),window.addEventListener(ce,t),l.onCleanup(()=>{window.removeEventListener(Pe,e),window.removeEventListener(ce,t)})});let X="",T=0,ke=0,Se="";const Q=e=>{try{const t=e.querySelector('[data-slot="user-message-text"]')??e.querySelector('[data-slot="user-message-body"]')??e;t.classList.remove("sr-flash-ring"),t.offsetWidth,t.classList.add("sr-flash-ring"),setTimeout(()=>t.classList.remove("sr-flash-ring"),1900)}catch{}},Fe=()=>{for(const e of d())if(y(e.id))return e},ee=(e,t)=>{bt(`${e}#${le}${t}`),X=t},$e=()=>{const t=C()?.querySelectorAll("[data-message-id]")??[];if(t.length===0)return"0";const r=t[0].getAttribute("data-message-id")??"",i=t[t.length-1].getAttribute("data-message-id")??"";return`${t.length}:${r}:${i}`},Ue=(e,t,r,i)=>new Promise(n=>{let c=!1,u,g,b;const m=Date.now();let f=$e(),h=m,A=0;const k=G=>{if(!c){c=!0;try{u?.disconnect()}catch{}clearInterval(g),clearTimeout(b),n(G??null)}},M=G=>{if(t!==T)return k(null);const Ae=G?y(e):document.getElementById(`${le}${e}`);if(Ae)return k(Ae);if(!G)return;const Me=$e(),Ie=Date.now();if(Me!==f){f=Me,h=Ie,A++,i?.(A);return}const Ze=A>0?ut:pt;Ie-h>Ze&&k(null)},Te=y(e);if(Te)return k(Te);u=new MutationObserver(()=>M(!1)),u.observe(document.documentElement,{childList:!0,subtree:!0}),g=setInterval(()=>M(!0),120),b=setTimeout(()=>k(y(e)),Math.max(400,r))}),Ee=()=>{T++,B(!1),D(0),P(0),J(0),N(""),U("已取消跳转")},Ge=async e=>{if(te()&&Ve(),x()&&O()===e.id)return;const t=++T;B(!1),D(0),P(0),J(0),N(""),pe=e.id;const r=Date.now();if(e.id===Se&&r-ke<gt){const w=y(e.id),f=C();if(w&&f){ie(w),Q(w);return}}ke=r,Se=e.id;const i=C(),n=y(e.id);if(n&&i){ie(n),Q(n);return}const c=qe(s);if(!c){U(`无法读取当前路由,无法定位 · ${oe(s)}`);return}const u=v();B(!0),N(e.id),D(0),P(0),I(!0);const g=Date.now(),b=setInterval(()=>{P(Math.round((Date.now()-g)/1e3))},500);let m=0;try{const w=Date.now()+ct;for(let f=0;f<lt&&Date.now()<w;f++){if(t!==T||v()!==u)return;if(J(f+1),X===e.id){const M=Fe()??d()[0];M&&(ee(c,M.id),await se(ft))}if(t!==T||v()!==u)return;ee(c,e.id);let h=0;const A=Math.min(dt,w-Date.now()),k=await Ue(e.id,t,A,M=>{h=M,D(m+M)});if(m+=h,t!==T||v()!==u)return;if(k){ie(k),Q(k);return}}t===T&&U(`未能定位 · ${oe(s)}`)}finally{clearInterval(b),t===T&&(B(!1),D(0),P(0),J(0),N(""))}},[te,re]=l.createSignal(!1);let R=0,ne="";const We=()=>{const e=C();if(!e)return;const t=e.getBoundingClientRect();for(const r of e.querySelectorAll("[data-message-id]")){const i=r.getBoundingClientRect();if(i.bottom<=t.top+1)continue;const n=r.getAttribute("data-message-id");if(n)return{el:r,id:n,offset:i.top-t.top}}},_e=e=>{if(!e)return;const t=C();if(!t)return;const r=e.el?.isConnected?e.el:y(e.id);if(!r)return;const i=t.getBoundingClientRect(),n=r.getBoundingClientRect().top-i.top-e.offset;Math.abs(n)>2&&(t.scrollTop+=n)},Ve=()=>{R++,re(!1)},Ce=()=>{const e=v();!e||te()||ne!==e&&(d().length<15||Ye())},Ye=async()=>{if(te())return;const e=qe(s),t=d()[0],r=v();if(!e||!t||!r||ne===r&&y(t.id))return;const i=++R,n=We(),c=C(),u=c?c.scrollTop+c.clientHeight>=c.scrollHeight-32:!0;re(!0);try{ee(e,t.id);const g=Date.now()+12e4,b=setInterval(()=>_e(n),150);try{for(;Date.now()<g&&i===R&&!y(t.id);)await se(250)}finally{clearInterval(b)}if(i!==R)return;if(_e(n),u){const m=C();m&&(m.scrollTop=m.scrollHeight)}ne=r}finally{i===R&&re(!1)}};l.createEffect(()=>{x()&&O()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Ke=[o.createComponent(l.Show,{get when(){return L()},get children(){var e=Le();return o.insert(e,L),e}}),(()=>{var e=et(),t=e.firstChild,r=t.firstChild,i=t.nextSibling;return e.addEventListener("mouseleave",()=>{x()||I(!1)}),e.addEventListener("mouseenter",()=>{I(!0),Ce()}),o.insert(t,o.createComponent(l.Show,{get when(){return x()},get fallback(){return(()=>{var n=tt();return n.firstChild,o.insert(n,(()=>{var c=o.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),n})()},get children(){var n=Xe();return n.firstChild,o.insert(n,(()=>{var c=o.memo(()=>he()>0);return()=>c()?` · ${he()} 页`:""})(),null),o.insert(n,(()=>{var c=o.memo(()=>me()>0);return()=>c()?` · ${me()}s`:""})(),null),n}}),r),r.$$click=()=>{x()?Ee():I(!1)},o.insert(i,o.createComponent(l.Show,{get when(){return d().length>0},get fallback(){return(()=>{var n=rt();return o.insert(n,()=>Je()||`… ${De}`),n})()},get children(){return o.createComponent(l.For,{get each(){return d()},children:(n,c)=>(()=>{var u=st(),g=u.firstChild,b=g.firstChild;b.firstChild;var m=b.nextSibling,w=g.nextSibling;return u.$$click=()=>{Ge(n)},o.insert(b,()=>c()+1,null),o.insert(m,()=>ht(n.created)),o.insert(g,()=>n.text,null),o.insert(u,o.createComponent(l.Show,{get when(){return o.memo(()=>!!x())()&&O()===n.id},get children(){return nt()}}),w),o.effect(f=>{var h=S()===n.id?"true":"false",A=x()&&O()===n.id?"true":"false";return h!==f.e&&o.setAttribute(u,"data-active",f.e=h),A!==f.t&&o.setAttribute(u,"data-busy",f.t=A),f},{e:void 0,t:void 0}),u})()})}})),o.insert(e,o.createComponent(l.Show,{get when(){return o.memo(()=>!!E())()&&L()},get children(){var n=Qe();return o.insert(n,L),n}}),null),o.effect(n=>{var c=E()?"true":"false",u=x()?"true":"false",g=x()?"取消跳转 (Esc)":"关闭 (Esc)";return c!==n.e&&o.setAttribute(e,"data-open",n.e=c),u!==n.t&&o.setAttribute(e,"data-jumping",n.t=u),g!==n.a&&o.setAttribute(r,"title",n.a=g),n},{e:void 0,t:void 0,a:void 0}),e})(),o.createComponent(l.Show,{get when(){return o.memo(()=>!E())()&&L()},get children(){var e=Le();return o.insert(e,L),e}})];return o.createComponent(o.Portal,{get mount(){return document.body},children:Ke})}const vt=q.Plugin.define({i18n:{en:{"command.jump":"Session turns: show / jump","command.jump.description":"Reveal the turn rail; click a turn to jump to that message"}},setup(a){a.add(q.Style,wt),a.add(q.Slot,{at:"window.bottom",render:()=>o.createComponent(l.Show,{get when(){return a.sessions.current()},get children(){return o.createComponent(xt,{session:()=>a.sessions.current()})}})}),a.add(q.Command,()=>({id:"jump",title:a.t("command.jump"),description:a.t("command.jump.description"),group:a.t("command.jump"),section:"navigation",bind:"mod+alt+r",slash:{name:"turns"},run:()=>{window.dispatchEvent(new Event(ce))}}))}});o.delegateEvents(["click"]);exports.default=vt;
245
+ `;function kt(l){const o=H.usePlugin(),[d,p]=a.createSignal([]),[E,_]=a.createSignal(""),[C,M]=a.createSignal(!1),[Fe,Ue]=a.createSignal(""),[D,be]=a.createSignal(""),[y,F]=a.createSignal(!1),[we,P]=a.createSignal(0),[xe,j]=a.createSignal(0),[$t,R]=a.createSignal(0),[U,W]=a.createSignal(""),[ve,q]=a.createSignal(!1),S=new Map;let re=0,B=!1,G=!1,V="",J;const h=()=>l.session()?.id,ne=()=>{const e=[...S.values()].sort((r,i)=>r.created-i.created),t=a.untrack(d);t.length===e.length&&t.every((r,i)=>r===e[i])||(p(e),He=e.length)},ye=e=>{const t=S.get(e.id);t&&t.created===e.time&&t.text===e.text||S.set(e.id,{id:e.id,created:e.time,text:e.text})},We=async(e,t,r)=>{const i=r?{sessionID:e,type:"user",limit:t,cursor:r}:{sessionID:e,type:"user",limit:t,order:"desc"};return await o.client.message.list(i)},se=async(e,t)=>{const r=++re;B=!0,t==="full"&&(z="");let i="";try{let n;const c=t==="full"?lt:1,u=t==="full"?ct:dt;let m=t!=="full";for(let w=0;w<c;w++){let b;try{b=await We(e,u,n)}catch(g){i=String(g?.message??g);break}if(r!==re)return;m||(S.clear(),m=!0);let x=0;for(const g of b?.data??[])S.has(g.id)||(ye({id:g.id,time:g.time?.created??0,text:Oe(g)}),x++);ne();const f=b?.cursor?.next;if(!f||x===0)break;n=f}}finally{B=!1,V=e,fe=i,Ue(S.size===0?i||"暂无用户消息":""),G&&(G=!1,ke()),t==="full"&&!i&&e===a.untrack(h)&&S.size>=15&&setTimeout(()=>Q(),800)}},ke=()=>{J||(J=setTimeout(()=>{J=void 0;const e=a.untrack(h);e&&se(e,"light")},300))},Se=e=>{if(!(!e||e!==a.untrack(h))){if(B){G=!0;return}ke()}},Ge=e=>{const t=e?.type;if(t==="message.updated"){const r=e?.data??{},i=r.info;i?.role==="user"&&i?.id&&(ye({id:i.id,time:i.time?.created??0,text:Oe(i)}),ne()),Se(r.sessionID)}else if(t==="message.removed"){const r=e?.data??{};r.messageID&&S.delete(r.messageID)&&ne(),Se(r.sessionID)}else if(t==="location.shutdown"){const r=a.untrack(h);r&&V===r&&se(r,"full")}};a.onMount(()=>{let e=!1,t=0,r;(async()=>{for(;!e;){const c=o.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!e;){const u=await c.next();if(u.done)break;t=0,Ge(u.value)}}catch{}try{await c.return?.()}catch{}if(e)break;t++,await le(Math.min(2e3*t,15e3))}})(),a.onCleanup(()=>{e=!0,clearTimeout(r),J&&clearTimeout(J)})}),a.createEffect(()=>{const e=h();e&&a.untrack(()=>{V!==e&&(re++,B=!1,G=!1,S.clear(),p([]),_(""),ae="",se(e,"full"))})});let Y;const I=()=>(Y?.isConnected||(Y=me()),Y),Ve=()=>{const e=I();if(!e)return;const t=e.querySelectorAll("[data-message-id]");if(t.length===0)return;const r=e.getBoundingClientRect().top+40;for(const i of t)if(i.getBoundingClientRect().bottom>=r){const n=i.getAttribute("data-message-id");n&&_(n);break}};let oe=!1;const $e=()=>{oe||(oe=!0,requestAnimationFrame(()=>{oe=!1,Ve()}))};a.onMount(()=>{const e=()=>$e();document.addEventListener("scroll",e,!0);const t=setInterval($e,1500);a.onCleanup(()=>{document.removeEventListener("scroll",e,!0),clearInterval(t)})});let Ee=0;const K=(e,t=2600)=>{const r=++Ee;be(e),setTimeout(()=>{r===Ee&&be("")},t)};a.onMount(()=>{const e=r=>{if(r.key==="Escape"){if(a.untrack(y)){Me(),r.stopPropagation();return}a.untrack(C)&&(M(!1),r.stopPropagation())}},t=r=>{if(!a.untrack(C))return;(r.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||M(!1)};window.addEventListener("keydown",e,!0),document.addEventListener("pointerdown",t,!0),a.onCleanup(()=>{window.removeEventListener("keydown",e,!0),document.removeEventListener("pointerdown",t,!0)})}),a.onMount(()=>{const e=()=>K(`${Be} · ${ce(o)}`),t=()=>{M(r=>!r),Q()};window.addEventListener(Je,e),window.addEventListener(pe,t),a.onCleanup(()=>{window.removeEventListener(Je,e),window.removeEventListener(pe,t)})});let ae="",v=0,_e=0,Ce="";const Z=e=>{try{const t=e.querySelector('[data-slot="user-message-text"]')??e.querySelector('[data-slot="user-message-body"]')??e;t.classList.remove("sr-flash-ring"),t.offsetWidth,t.classList.add("sr-flash-ring"),setTimeout(()=>t.classList.remove("sr-flash-ring"),1900)}catch{}},Ye=()=>{for(const e of d())if(k(e.id))return e},O=(e,t)=>{vt(`${e}#${ue}${t}`),ae=t},Te=()=>{const t=I()?.querySelectorAll("[data-message-id]")??[];if(t.length===0)return"0";const r=t[0].getAttribute("data-message-id")??"",i=t[t.length-1].getAttribute("data-message-id")??"";return`${t.length}:${r}:${i}`},Ae=(e,t,r,i)=>new Promise(n=>{let c=!1,u,m,w;const b=Date.now();let f=Te(),g=b,T=0;const $=ee=>{if(!c){c=!0;try{u?.disconnect()}catch{}clearInterval(m),clearTimeout(w),n(ee??null)}},A=ee=>{if(t!==v)return $(null);const Pe=ee?k(e):document.getElementById(`${ue}${e}`);if(Pe)return $(Pe);if(!ee)return;const je=Te(),Re=Date.now();if(je!==f){f=je,g=Re,T++,i?.(T);return}const et=T>0?gt:ht;Re-g>et&&$(null)},De=k(e);if(De)return $(De);u=new MutationObserver(()=>A(!1)),u.observe(document.documentElement,{childList:!0,subtree:!0}),m=setInterval(()=>A(!0),120),w=setTimeout(()=>$(k(e)),Math.max(400,r))}),Me=()=>{v++,F(!1),P(0),j(0),R(0),W(""),q(!1),K("已取消跳转")},Ke=async e=>{if(y()&&U()===e.id)return;const t=ze(o),r=I(),i=k(e.id);if(i&&r){L()&&(X(),t&&O(t,e.id)),te(i),Z(i);return}const n=Date.now();if(e.id===Ce&&n-_e<bt){const x=k(e.id);if(x&&r){te(x),Z(x);return}}if(_e=n,Ce=e.id,he=e.id,!t){K(`无法读取当前路由,无法定位 · ${ce(o)}`);return}const c=++v,u=h();F(!0),W(e.id),P(0),j(0),R(0),q(L()),M(!0);const m=Date.now(),w=setInterval(()=>{j(Math.round((Date.now()-m)/1e3))},500);let b=0;try{if(L()){const f=await Ae(e.id,c,12e4,g=>P(g));if(c!==v||h()!==u)return;if(f){X(),O(t,e.id),te(f),Z(f);return}if(L()&&X(),q(!1),c!==v||h()!==u)return}const x=Date.now()+pt;for(let f=0;f<ut&&Date.now()<x;f++){if(c!==v||h()!==u)return;if(R(f+1),ae===e.id){const A=Ye()??d()[0];A&&(O(t,A.id),await le(mt))}if(c!==v||h()!==u)return;O(t,e.id);let g=0;const T=Math.min(ft,x-Date.now()),$=await Ae(e.id,c,T,A=>{g=A,P(b+A)});if(b+=g,c!==v||h()!==u)return;if($){te($),Z($);return}}c===v&&K(`未能定位 · ${ce(o)}`)}finally{clearInterval(w),c===v&&(F(!1),P(0),j(0),R(0),W(""),q(!1))}},[L,ie]=a.createSignal(!1);let N=0,z="";const Ze=()=>{const e=I();if(!e)return;const t=e.getBoundingClientRect();for(const r of e.querySelectorAll("[data-message-id]")){const i=r.getBoundingClientRect();if(i.bottom<=t.top+1)continue;const n=r.getAttribute("data-message-id");if(n)return{el:r,id:n,offset:i.top-t.top}}},Ie=e=>{if(!e)return;const t=I();if(!t)return;const r=e.el?.isConnected?e.el:k(e.id);if(!r)return;const i=t.getBoundingClientRect(),n=r.getBoundingClientRect().top-i.top-e.offset;Math.abs(n)>2&&(t.scrollTop+=n)},X=()=>{N++,ie(!1)},Q=()=>{const e=h();!e||L()||z!==e&&(d().length<15||Xe())},Xe=async()=>{if(L())return;const e=ze(o),t=d()[0],r=h();if(!e||!t||!r||z===r&&k(t.id))return;const i=++N,n=Ze(),c=I(),u=c?c.scrollTop+c.clientHeight>=c.scrollHeight-32:!0;ie(!0);try{O(e,t.id);const m=Date.now()+12e4,w=setInterval(()=>Ie(n),150);try{for(;Date.now()<m&&i===N&&!k(t.id);)await le(250)}finally{clearInterval(w)}if(i!==N)return;if(Ie(n),u){const b=I();b&&(b.scrollTop=b.scrollHeight)}z=r}finally{i===N&&ie(!1)}};let Le=a.untrack(h);a.createEffect(()=>{const e=h();e!==Le&&(Le=e,X(),v++,F(!1),P(0),j(0),R(0),W(""),q(!1))}),a.createEffect(()=>{const e=h(),t=d().length,r=L();if(!e||t<15||r||z===e||B||V!==e)return;const i=setTimeout(()=>Q(),800);a.onCleanup(()=>clearTimeout(i))}),a.createEffect(()=>{y()&&U()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Qe=[s.createComponent(a.Show,{get when(){return D()},get children(){var e=qe();return s.insert(e,D),e}}),(()=>{var e=nt(),t=e.firstChild,r=t.firstChild,i=t.nextSibling;return e.addEventListener("mouseleave",()=>{y()||M(!1)}),e.addEventListener("mouseenter",()=>{M(!0),Q()}),s.insert(t,s.createComponent(a.Show,{get when(){return y()},get fallback(){return(()=>{var n=st();return n.firstChild,s.insert(n,(()=>{var c=s.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),n})()},get children(){var n=tt();return s.insert(n,()=>ve()?"预热中 · 稍等一下":"加载历史中",null),s.insert(n,(()=>{var c=s.memo(()=>we()>0);return()=>c()?` · ${we()} 页`:""})(),null),s.insert(n,(()=>{var c=s.memo(()=>xe()>0);return()=>c()?` · ${xe()}s`:""})(),null),n}}),r),r.$$click=()=>{y()?Me():M(!1)},s.insert(i,s.createComponent(a.Show,{get when(){return d().length>0},get fallback(){return(()=>{var n=ot();return s.insert(n,()=>Fe()||`… ${Be}`),n})()},get children(){return s.createComponent(a.For,{get each(){return d()},children:(n,c)=>(()=>{var u=it(),m=u.firstChild,w=m.firstChild;w.firstChild;var b=w.nextSibling,x=m.nextSibling;return u.$$click=()=>{Ke(n)},s.insert(w,()=>c()+1,null),s.insert(b,()=>wt(n.created)),s.insert(m,()=>n.text,null),s.insert(u,s.createComponent(a.Show,{get when(){return s.memo(()=>!!y())()&&U()===n.id},get children(){var f=at();return s.insert(f,()=>ve()?"稍等":"加载中"),f}}),x),s.effect(f=>{var g=E()===n.id?"true":"false",T=y()&&U()===n.id?"true":"false";return g!==f.e&&s.setAttribute(u,"data-active",f.e=g),T!==f.t&&s.setAttribute(u,"data-busy",f.t=T),f},{e:void 0,t:void 0}),u})()})}})),s.insert(e,s.createComponent(a.Show,{get when(){return s.memo(()=>!!C())()&&D()},get children(){var n=rt();return s.insert(n,D),n}}),null),s.effect(n=>{var c=C()?"true":"false",u=y()?"true":"false",m=y()?"取消跳转 (Esc)":"关闭 (Esc)";return c!==n.e&&s.setAttribute(e,"data-open",n.e=c),u!==n.t&&s.setAttribute(e,"data-jumping",n.t=u),m!==n.a&&s.setAttribute(r,"title",n.a=m),n},{e:void 0,t:void 0,a:void 0}),e})(),s.createComponent(a.Show,{get when(){return s.memo(()=>!C())()&&D()},get children(){var e=qe();return s.insert(e,D),e}})];return s.createComponent(s.Portal,{get mount(){return document.body},children:Qe})}const St=H.Plugin.define({i18n:{en:{"command.jump":"Session turns: show / jump","command.jump.description":"Reveal the turn rail; click a turn to jump to that message"}},setup(l){l.add(H.Style,yt),l.add(H.Slot,{at:"window.bottom",render:()=>s.createComponent(a.Show,{get when(){return l.sessions.current()},get children(){return s.createComponent(kt,{session:()=>l.sessions.current()})}})}),l.add(H.Command,()=>({id:"jump",title:l.t("command.jump"),description:l.t("command.jump.description"),group:l.t("command.jump"),section:"navigation",bind:"mod+alt+r",slash:{name:"turns"},run:()=>{window.dispatchEvent(new Event(pe))}}))}});s.delegateEvents(["click"]);exports.default=St;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-session-turns",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "OpenCode Desktop 插件:在会话右缘列出每一轮用户提问,点击即可跳转到对应消息。A right-edge rail of user turns; click one to jump to that message.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/gui.tsx CHANGED
@@ -6,7 +6,7 @@ import { Portal } from "solid-js/web"
6
6
  // constants / helpers
7
7
  // ---------------------------------------------------------------------------
8
8
 
9
- const VERSION = "v16"
9
+ const VERSION = "v19"
10
10
  const HASH_PREFIX = "message-"
11
11
  const DIAG_EVENT = "session-turns:diag"
12
12
  const OPEN_EVENT = "session-turns:open"
@@ -455,6 +455,9 @@ function Rail(props) {
455
455
  const [jumpElapsed, setJumpElapsed] = createSignal(0)
456
456
  const [jumpChain, setJumpChain] = createSignal(0)
457
457
  const [busyID, setBusyID] = createSignal("")
458
+ // True while a click is waiting for the background prewarm to page its target
459
+ // in (header reads "预热中 · 稍等一下" instead of "加载历史中").
460
+ const [waitWarm, setWaitWarm] = createSignal(false)
458
461
 
459
462
  // ---- turn cache ---------------------------------------------------------
460
463
  // Object identity is kept stable across refreshes so Solid's <For> reuses
@@ -497,6 +500,9 @@ function Rail(props) {
497
500
  const load = async (id, mode) => {
498
501
  const token = ++loadToken
499
502
  loadingActive = true
503
+ // A full (re)load means the host store may be cold again (session open or
504
+ // server rebuild): invalidate the prewarm marker so it runs once more.
505
+ if (mode === "full") warmedFor = ""
500
506
  let error = ""
501
507
  try {
502
508
  let cursor
@@ -543,6 +549,11 @@ function Rail(props) {
543
549
  pendingBump = false
544
550
  scheduleLight()
545
551
  }
552
+ // Prewarm long sessions as soon as their full turn list has loaded, so
553
+ // the history is warm before the first click (not only on rail hover).
554
+ if (mode === "full" && !error && id === untrack(sessionID) && cache.size >= 15) {
555
+ setTimeout(() => maybePrewarm(), 800)
556
+ }
546
557
  }
547
558
  }
548
559
 
@@ -870,36 +881,40 @@ function Rail(props) {
870
881
  setJumpElapsed(0)
871
882
  setJumpChain(0)
872
883
  setBusyID("")
884
+ setWaitWarm(false)
873
885
  flash("已取消跳转")
874
886
  }
875
887
 
876
888
  const jump = async (turn) => {
877
- // A jump takes priority over a background prewarm.
878
- if (warming()) cancelWarm()
879
-
880
889
  // Clicking the same turn again while its jump is running must NOT restart
881
890
  // the loop: every restart cancels the host's in-flight page load, which is
882
891
  // what makes a deep jump advance only one page per click. The header
883
892
  // already shows the progress, so just leave it running.
884
893
  if (jumping() && busyID() === turn.id) return
885
894
 
886
- const token = ++jumpToken
887
- // A new click always owns the progress UI. This also clears a previous
888
- // jump's state when the new target takes the mounted fast path, where the
889
- // superseded loop's finally no longer touches these signals.
890
- setJumping(false)
891
- setJumpPages(0)
892
- setJumpElapsed(0)
893
- setJumpChain(0)
894
- setBusyID("")
895
- lastJumpID = turn.id
896
- const now = Date.now()
895
+ const path = routePath(ctx)
896
+ const sc = scroller()
897
+ const mounted = findRow(turn.id)
898
+
899
+ // Fast path: the row is already mounted (e.g. the prewarm reached it), so
900
+ // the jump is instant. If a prewarm is still running, re-target the host's
901
+ // pending oldest-seek to this turn first - otherwise the host would scroll
902
+ // to the oldest message when that seek completes and yank the viewport.
903
+ if (mounted && sc) {
904
+ if (warming()) {
905
+ cancelWarm()
906
+ if (path) navTo(path, turn.id)
907
+ }
908
+ alignAfterLand(mounted)
909
+ pulse(mounted)
910
+ return
911
+ }
897
912
 
913
+ const now = Date.now()
898
914
  // Same target clicked again shortly after: treat as idempotent re-scroll
899
915
  // instead of restarting the paging loop.
900
916
  if (turn.id === lastJumpTurnID && now - lastJumpAt < SAME_TARGET_WINDOW_MS) {
901
917
  const row = findRow(turn.id)
902
- const sc = scroller()
903
918
  if (row && sc) {
904
919
  alignAfterLand(row)
905
920
  pulse(row)
@@ -908,45 +923,23 @@ function Rail(props) {
908
923
  }
909
924
  lastJumpAt = now
910
925
  lastJumpTurnID = turn.id
926
+ lastJumpID = turn.id
911
927
 
912
- const sc = scroller()
913
- const row = findRow(turn.id)
914
-
915
- // Fast path: the row is mounted, scroll locally. No toast: the bubble
916
- // ring is the feedback.
917
- if (row && sc) {
918
- alignAfterLand(row)
919
- pulse(row)
920
- return
921
- }
922
-
923
- const path = routePath(ctx)
924
928
  if (!path) {
925
929
  flash(`无法读取当前路由,无法定位 · ${diagnostics(ctx)}`)
926
930
  return
927
931
  }
928
932
 
929
- // Deep history.
930
- //
931
- // The host's own seek machine does the heavy lifting: hash.changed ->
932
- // applying -> pending(target) -> target.synced -> seek, and it loads the
933
- // missing history asynchronously (up to 12 internal retries). Every extra
934
- // hash change we send RESETS that pending state and aborts the history
935
- // fetch in flight, which is why hammering the hash made a deep jump
936
- // advance a single page at a time.
937
- //
938
- // So: set the hash once, then be patient. Only when the host has been
939
- // completely idle for JUMP_IDLE_MS do we bounce the hash through a
940
- // mounted turn to kick off a fresh chain.
933
+ const token = ++jumpToken
941
934
  const sessionAtJump = sessionID()
942
935
  setJumping(true)
943
936
  setBusyID(turn.id)
944
937
  setJumpPages(0)
945
938
  setJumpElapsed(0)
939
+ setJumpChain(0)
940
+ setWaitWarm(warming())
946
941
  // Pin the panel open even if the pointer has left it meanwhile.
947
942
  setOpen(true)
948
- // The loading state is shown in the panel header and on the target row -
949
- // no toast needed, the toast would land outside the user's focus.
950
943
  const startedAt = Date.now()
951
944
  const elapsedTimer = setInterval(() => {
952
945
  setJumpElapsed(Math.round((Date.now() - startedAt) / 1000))
@@ -955,6 +948,34 @@ function Rail(props) {
955
948
  // the per-chain counter, the headline must not go backwards).
956
949
  let chainBase = 0
957
950
  try {
951
+ // A prewarm is already paging the history (it seeks the oldest turn, so
952
+ // it passes through this one on the way). Don't fight it with a competing
953
+ // seek: just wait for the row to mount, then land. The header reads
954
+ // "预热中 · 稍等一下" while we wait.
955
+ if (warming()) {
956
+ const landed = await waitForRow(turn.id, token, 120000, (pages) => setJumpPages(pages))
957
+ if (token !== jumpToken || sessionID() !== sessionAtJump) return
958
+ if (landed) {
959
+ cancelWarm()
960
+ navTo(path, turn.id)
961
+ alignAfterLand(landed)
962
+ pulse(landed)
963
+ return
964
+ }
965
+ // The prewarm ran out without mounting the row: fall back to driving
966
+ // the host's seek ourselves below.
967
+ if (warming()) cancelWarm()
968
+ setWaitWarm(false)
969
+ if (token !== jumpToken || sessionID() !== sessionAtJump) return
970
+ }
971
+
972
+ // Deep history, no prewarm. The host's own seek machine does the heavy
973
+ // lifting: hash.changed -> applying -> pending(target) -> target.synced
974
+ // -> seek, loading the missing history asynchronously. Every extra hash
975
+ // change RESETS that pending state and aborts the fetch in flight, which
976
+ // is why hammering the hash made a deep jump advance one page per click.
977
+ // So: set the hash once, then be patient; only bounce it through a
978
+ // mounted turn when the host has been idle for JUMP_IDLE_MS.
958
979
  const deadline = Date.now() + JUMP_TOTAL_MS
959
980
  for (let chain = 0; chain < JUMP_ROUNDS && Date.now() < deadline; chain++) {
960
981
  // Abandon the loop if the user switched sessions mid-jump: toggling the
@@ -1005,6 +1026,7 @@ function Rail(props) {
1005
1026
  setJumpElapsed(0)
1006
1027
  setJumpChain(0)
1007
1028
  setBusyID("")
1029
+ setWaitWarm(false)
1008
1030
  }
1009
1031
  }
1010
1032
  }
@@ -1102,6 +1124,37 @@ function Rail(props) {
1102
1124
  }
1103
1125
  }
1104
1126
 
1127
+ // Session switch: abort any jump/prewarm in flight - they target the old
1128
+ // route, and toggling its hash would navigate back to the previous session.
1129
+ let guardSid = untrack(sessionID)
1130
+ createEffect(() => {
1131
+ const sid = sessionID()
1132
+ if (sid === guardSid) return
1133
+ guardSid = sid
1134
+ cancelWarm()
1135
+ jumpToken++
1136
+ setJumping(false)
1137
+ setJumpPages(0)
1138
+ setJumpElapsed(0)
1139
+ setJumpChain(0)
1140
+ setBusyID("")
1141
+ setWaitWarm(false)
1142
+ })
1143
+
1144
+ // Resume the prewarm after a jump interrupted it, so the remaining older
1145
+ // history still gets warmed for the next click. The initial start happens in
1146
+ // load()'s finally (once the full turn list is in); this only fires when the
1147
+ // warming signal flips back to false while the session is not fully warm.
1148
+ createEffect(() => {
1149
+ const sid = sessionID()
1150
+ const count = turns().length
1151
+ const hot = warming()
1152
+ if (!sid || count < 15 || hot || warmedFor === sid) return
1153
+ if (loadingActive || loadedFor !== sid) return
1154
+ const timer = setTimeout(() => maybePrewarm(), 800)
1155
+ onCleanup(() => clearTimeout(timer))
1156
+ })
1157
+
1105
1158
  // Keep the busy row visible in the panel list while the jump pages through
1106
1159
  // history, so the "which turn am I waiting for" answer stays on screen.
1107
1160
  createEffect(() => {
@@ -1144,7 +1197,8 @@ function Rail(props) {
1144
1197
  }
1145
1198
  >
1146
1199
  <span class="sr-title sr-progress">
1147
- 加载历史中{jumpPages() > 0 ? ` · ${jumpPages()} 页` : ""}
1200
+ {waitWarm() ? "预热中 · 稍等一下" : "加载历史中"}
1201
+ {jumpPages() > 0 ? ` · ${jumpPages()} 页` : ""}
1148
1202
  {jumpElapsed() > 0 ? ` · ${jumpElapsed()}s` : ""}
1149
1203
  </span>
1150
1204
  </Show>
@@ -1177,7 +1231,7 @@ function Rail(props) {
1177
1231
  {turn.text}
1178
1232
  </span>
1179
1233
  <Show when={jumping() && busyID() === turn.id}>
1180
- <span class="sr-badge">加载中</span>
1234
+ <span class="sr-badge">{waitWarm() ? "稍等" : "加载中"}</span>
1181
1235
  </Show>
1182
1236
  <span class="sr-dash" />
1183
1237
  </button>