opencode-session-turns 0.1.15 → 0.1.16

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.
@@ -1,5 +1,5 @@
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=`
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const s=require("solid-js/web"),F=require("@opencode/plugin/gui"),a=require("solid-js");var Je=s.template("<div class=sr-status>"),nt=s.template('<span class="sr-title sr-progress">'),st=s.template("<div class=sr-foot>"),ot=s.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),it=s.template("<span class=sr-title>会话轮次"),at=s.template("<div class=sr-note>"),lt=s.template("<span class=sr-badge>"),ct=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 Oe="v20",fe="message-",Ne="session-turns:diag",ge="session-turns:open",dt=40,ut=200,pt=50,ft=30,gt=45e3,ht=2e4,mt=8e3,bt=12e3,ze=220,wt=800;let We=0,he="",me="",be="";const Fe=l=>{const o=String(l?.text??"").split(`
2
+ `).map(f=>f.trim()).find(f=>f.length>0);if(o)return o;const d=l?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},He=l=>String(l).padStart(2,"0"),xt=l=>{const o=new Date(l??0);return`${He(o.getHours())}:${He(o.getMinutes())}`},re=l=>new Promise(o=>setTimeout(o,l)),we=()=>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}})(),$=l=>{const o=document.getElementById(`${fe}${l}`);if(o)return o;const d=`[data-message-id="${l.replace(/"/g,'\\"')}"]`;for(const f of document.querySelectorAll(d))if(f.getAttribute("data-timeline-row")==="UserMessage"||f.querySelector('[data-slot="user-message-text"]'))return f},ue=l=>{const o=we(),d=[...document.querySelectorAll("[data-message-id]")],f=d[0]?.getAttribute("data-message-id")??"",C=be?` · 目标 ${$(be)?"在":"不在"} DOM`:"",T=f?` · 行ID ${f.slice(0,20)}`:"";let A="(空)";try{A=l.router.path()||"(空)"}catch{}return`轮次 ${We} · 容器 ${o?"OK":"缺失"} · DOM ${d.length} 行${C}${T} · 路由 ${A} · hash ${window.location.hash||"(空)"}${me?` · ${me}`:""}${he?` · ${he}`:""}`},vt=l=>{const o=l.getBoundingClientRect().top;let d=o;for(const f of l.querySelectorAll(".sticky, [data-session-title]")){if(!(f instanceof HTMLElement)||getComputedStyle(f).position!=="sticky"&&!f.hasAttribute("data-session-title"))continue;const T=f.getBoundingClientRect();T.height!==0&&(T.top>o+4||(d=Math.max(d,T.bottom)))}return d},pe=l=>{try{const o=we();if(!o||!l.isConnected)return;const d=l.querySelector('[data-slot="user-message-text"]')??l,f=vt(o),C=d.getBoundingClientRect().top-(f+18);Math.abs(C)>2&&(o.scrollTop+=C),me=`inset ${Math.round(f)} · delta ${Math.round(C)}`}catch{}},ne=l=>{pe(l),setTimeout(()=>pe(l),450),setTimeout(()=>pe(l),1e3)},Ue=l=>{let o="";try{o=l.router.path()||""}catch{}return o.startsWith("/")?o:""},yt=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()}},kt=`
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 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;
245
+ `;function St(l){const o=F.usePlugin(),[d,f]=a.createSignal([]),[C,T]=a.createSignal(""),[A,M]=a.createSignal(!1),[Ge,Ve]=a.createSignal(""),[j,xe]=a.createSignal(""),[E,H]=a.createSignal(!1),[ve,R]=a.createSignal(0),[ye,q]=a.createSignal(0),[Et,B]=a.createSignal(0),[U,W]=a.createSignal(""),[ke,J]=a.createSignal(!1),_=new Map;let se=0,O=!1,G=!1,V="",N;const h=()=>l.session()?.id,oe=()=>{const e=[..._.values()].sort((r,i)=>r.created-i.created),t=a.untrack(d);t.length===e.length&&t.every((r,i)=>r===e[i])||(f(e),We=e.length)},Se=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})},Ye=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)},ie=async(e,t)=>{const r=++se;O=!0,t==="full"&&(z="",de="");let i="";try{let n;const c=t==="full"?dt:1,u=t==="full"?ut:pt;let m=t!=="full";for(let x=0;x<c;x++){let v;try{v=await Ye(e,u,n)}catch(g){i=String(g?.message??g);break}if(r!==se)return;m||(_.clear(),m=!0);let b=0;for(const g of v?.data??[])_.has(g.id)||(Se({id:g.id,time:g.time?.created??0,text:Fe(g)}),b++);oe();const p=v?.cursor?.next;if(!p||b===0)break;n=p}}finally{O=!1,V=e,he=i,Ve(_.size===0?i||"暂无用户消息":""),G&&(G=!1,$e()),t==="full"&&!i&&e===a.untrack(h)&&_.size>=15&&setTimeout(()=>ee(),1200)}},$e=()=>{N||(N=setTimeout(()=>{N=void 0;const e=a.untrack(h);e&&ie(e,"light")},300))},Ee=e=>{if(!(!e||e!==a.untrack(h))){if(O){G=!0;return}$e()}},Ke=e=>{const t=e?.type;if(t==="message.updated"){const r=e?.data??{},i=r.info;i?.role==="user"&&i?.id&&(Se({id:i.id,time:i.time?.created??0,text:Fe(i)}),oe()),Ee(r.sessionID)}else if(t==="message.removed"){const r=e?.data??{};r.messageID&&_.delete(r.messageID)&&oe(),Ee(r.sessionID)}else if(t==="location.shutdown"){const r=a.untrack(h);r&&V===r&&ie(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,Ke(u.value)}}catch{}try{await c.return?.()}catch{}if(e)break;t++,await re(Math.min(2e3*t,15e3))}})(),a.onCleanup(()=>{e=!0,clearTimeout(r),N&&clearTimeout(N)})}),a.createEffect(()=>{const e=h();e&&a.untrack(()=>{V!==e&&(se++,O=!1,G=!1,_.clear(),f([]),T(""),le="",ie(e,"full"))})});let Y;const I=()=>(Y?.isConnected||(Y=we()),Y),Ze=()=>{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&&T(n);break}};let ae=!1;const _e=()=>{ae||(ae=!0,requestAnimationFrame(()=>{ae=!1,Ze()}))};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 Ce=0;const K=(e,t=2600)=>{const r=++Ce;xe(e),setTimeout(()=>{r===Ce&&xe("")},t)};a.onMount(()=>{const e=r=>{if(r.key==="Escape"){if(a.untrack(E)){Le(),r.stopPropagation();return}a.untrack(A)&&(M(!1),r.stopPropagation())}},t=r=>{if(!a.untrack(A))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(`${Oe} · ${ue(o)}`),t=()=>{M(r=>!r),ee()};window.addEventListener(Ne,e),window.addEventListener(ge,t),a.onCleanup(()=>{window.removeEventListener(Ne,e),window.removeEventListener(ge,t)})});let le="",S=0,Te=0,Ae="";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{}},Me=()=>{for(const e of d())if($(e.id))return e},L=(e,t)=>{yt(`${e}#${fe}${t}`),le=t},X=()=>{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}`},Ie=(e,t,r,i)=>new Promise(n=>{let c=!1,u,m,x;const v=Date.now();let p=X(),g=v,w=0;const y=te=>{if(!c){c=!0;try{u?.disconnect()}catch{}clearInterval(m),clearTimeout(x),n(te??null)}},k=te=>{if(t!==S)return y(null);const Re=te?$(e):document.getElementById(`${fe}${e}`);if(Re)return y(Re);if(!te)return;const qe=X(),Be=Date.now();if(qe!==p){p=qe,g=Be,w++,i?.(w);return}const rt=w>0?mt:bt;Be-g>rt&&y(null)},je=$(e);if(je)return y(je);u=new MutationObserver(()=>k(!1)),u.observe(document.documentElement,{childList:!0,subtree:!0}),m=setInterval(()=>k(!0),120),x=setTimeout(()=>y($(e)),Math.max(400,r))}),Le=()=>{S++,H(!1),R(0),q(0),B(0),W(""),J(!1),K("已取消跳转")},Xe=async e=>{if(E()&&U()===e.id)return;const t=Ue(o),r=I(),i=$(e.id);if(i&&r){D()&&(Q(),t&&L(t,e.id)),ne(i),Z(i);return}const n=Date.now();if(e.id===Ae&&n-Te<wt){const b=$(e.id);if(b&&r){ne(b),Z(b);return}}if(Te=n,Ae=e.id,be=e.id,!t){K(`无法读取当前路由,无法定位 · ${ue(o)}`);return}const c=++S,u=h();H(!0),W(e.id),R(0),q(0),B(0),J(D()),M(!0);const m=Date.now(),x=setInterval(()=>{q(Math.round((Date.now()-m)/1e3))},500);let v=0;try{if(D()){const p=await Ie(e.id,c,2e4,g=>R(g));if(c!==S||h()!==u)return;if(p){Q(),L(t,e.id),ne(p),Z(p);return}if(D()&&Q(),J(!1),c!==S||h()!==u)return}const b=Date.now()+gt;for(let p=0;p<ft&&Date.now()<b;p++){if(c!==S||h()!==u)return;if(B(p+1),le===e.id){const k=Me()??d()[0];k&&(L(t,k.id),await re(ze))}if(c!==S||h()!==u)return;L(t,e.id);let g=0;const w=Math.min(ht,b-Date.now()),y=await Ie(e.id,c,w,k=>{g=k,R(v+k)});if(v+=g,c!==S||h()!==u)return;if(y){ne(y),Z(y);return}}c===S&&K(`未能定位 · ${ue(o)}`)}finally{clearInterval(x),c===S&&(H(!1),R(0),q(0),B(0),W(""),J(!1))}},[D,ce]=a.createSignal(!1);let P=0,z="",de="";const Qe=()=>{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}}},De=e=>{if(!e)return;const t=I();if(!t)return;const r=e.el?.isConnected?e.el:$(e.id);if(!r)return;const i=t.getBoundingClientRect(),n=r.getBoundingClientRect().top-i.top-e.offset;Math.abs(n)>2&&(t.scrollTop+=n)},Q=()=>{P++,ce(!1)},ee=()=>{const e=h();!e||D()||z!==e&&(d().length<15||et())},et=async()=>{if(D())return;const e=Ue(o),t=d()[0],r=h();if(!e||!t||!r||z===r&&$(t.id))return;const i=++P,n=Qe(),c=I(),u=c?c.scrollTop+c.clientHeight>=c.scrollHeight-32:!0;ce(!0);try{L(e,t.id);const m=Date.now()+6e4;let x=X(),v=Date.now(),b=0;const p=setInterval(()=>De(n),150);try{for(;Date.now()<m&&i===P&&!($(t.id)||(await re(250),i!==P));){const w=Date.now(),y=X();if(y!==x)x=y,v=w,b=0;else if(w-v>5e3){if(b>=2)break;const k=Me();if(k&&k.id!==t.id&&(L(e,k.id),await re(ze),i!==P))break;L(e,t.id),v=Date.now(),b++}}}finally{clearInterval(p)}if(i!==P)return;const g=!!$(t.id);if(De(n),u){const w=I();w&&(w.scrollTop=w.scrollHeight)}g?z=r:de=r}finally{i===P&&ce(!1)}};let Pe=a.untrack(h);a.createEffect(()=>{const e=h();e!==Pe&&(Pe=e,Q(),S++,H(!1),R(0),q(0),B(0),W(""),J(!1))}),a.createEffect(()=>{const e=h(),t=d().length,r=D();if(!e||t<15||r||z===e||de===e||O||V!==e)return;const i=setTimeout(()=>ee(),1200);a.onCleanup(()=>clearTimeout(i))}),a.createEffect(()=>{E()&&U()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const tt=[s.createComponent(a.Show,{get when(){return j()},get children(){var e=Je();return s.insert(e,j),e}}),(()=>{var e=ot(),t=e.firstChild,r=t.firstChild,i=t.nextSibling;return e.addEventListener("mouseleave",()=>{E()||M(!1)}),e.addEventListener("mouseenter",()=>{M(!0),ee()}),s.insert(t,s.createComponent(a.Show,{get when(){return E()},get fallback(){return(()=>{var n=it();return n.firstChild,s.insert(n,(()=>{var c=s.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),n})()},get children(){var n=nt();return s.insert(n,()=>ke()?"预热中 · 稍等一下":"加载历史中",null),s.insert(n,(()=>{var c=s.memo(()=>ve()>0);return()=>c()?` · ${ve()} 页`:""})(),null),s.insert(n,(()=>{var c=s.memo(()=>ye()>0);return()=>c()?` · ${ye()}s`:""})(),null),n}}),r),r.$$click=()=>{E()?Le():M(!1)},s.insert(i,s.createComponent(a.Show,{get when(){return d().length>0},get fallback(){return(()=>{var n=at();return s.insert(n,()=>Ge()||`… ${Oe}`),n})()},get children(){return s.createComponent(a.For,{get each(){return d()},children:(n,c)=>(()=>{var u=ct(),m=u.firstChild,x=m.firstChild;x.firstChild;var v=x.nextSibling,b=m.nextSibling;return u.$$click=()=>{Xe(n)},s.insert(x,()=>c()+1,null),s.insert(v,()=>xt(n.created)),s.insert(m,()=>n.text,null),s.insert(u,s.createComponent(a.Show,{get when(){return s.memo(()=>!!E())()&&U()===n.id},get children(){var p=lt();return s.insert(p,()=>ke()?"稍等":"加载中"),p}}),b),s.effect(p=>{var g=C()===n.id?"true":"false",w=E()&&U()===n.id?"true":"false";return g!==p.e&&s.setAttribute(u,"data-active",p.e=g),w!==p.t&&s.setAttribute(u,"data-busy",p.t=w),p},{e:void 0,t:void 0}),u})()})}})),s.insert(e,s.createComponent(a.Show,{get when(){return s.memo(()=>!!A())()&&j()},get children(){var n=st();return s.insert(n,j),n}}),null),s.effect(n=>{var c=A()?"true":"false",u=E()?"true":"false",m=E()?"取消跳转 (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(()=>!A())()&&j()},get children(){var e=Je();return s.insert(e,j),e}})];return s.createComponent(s.Portal,{get mount(){return document.body},children:tt})}const $t=F.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(F.Style,kt),l.add(F.Slot,{at:"window.bottom",render:()=>s.createComponent(a.Show,{get when(){return l.sessions.current()},get children(){return s.createComponent(St,{session:()=>l.sessions.current()})}})}),l.add(F.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(ge))}}))}});s.delegateEvents(["click"]);exports.default=$t;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-session-turns",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
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 = "v19"
9
+ const VERSION = "v20"
10
10
  const HASH_PREFIX = "message-"
11
11
  const DIAG_EVENT = "session-turns:diag"
12
12
  const OPEN_EVENT = "session-turns:open"
@@ -502,7 +502,10 @@ function Rail(props) {
502
502
  loadingActive = true
503
503
  // A full (re)load means the host store may be cold again (session open or
504
504
  // server rebuild): invalidate the prewarm marker so it runs once more.
505
- if (mode === "full") warmedFor = ""
505
+ if (mode === "full") {
506
+ warmedFor = ""
507
+ warmBlockedFor = ""
508
+ }
506
509
  let error = ""
507
510
  try {
508
511
  let cursor
@@ -552,7 +555,7 @@ function Rail(props) {
552
555
  // Prewarm long sessions as soon as their full turn list has loaded, so
553
556
  // the history is warm before the first click (not only on rail hover).
554
557
  if (mode === "full" && !error && id === untrack(sessionID) && cache.size >= 15) {
555
- setTimeout(() => maybePrewarm(), 800)
558
+ setTimeout(() => maybePrewarm(), 1200)
556
559
  }
557
560
  }
558
561
  }
@@ -953,7 +956,7 @@ function Rail(props) {
953
956
  // seek: just wait for the row to mount, then land. The header reads
954
957
  // "预热中 · 稍等一下" while we wait.
955
958
  if (warming()) {
956
- const landed = await waitForRow(turn.id, token, 120000, (pages) => setJumpPages(pages))
959
+ const landed = await waitForRow(turn.id, token, 20000, (pages) => setJumpPages(pages))
957
960
  if (token !== jumpToken || sessionID() !== sessionAtJump) return
958
961
  if (landed) {
959
962
  cancelWarm()
@@ -1041,6 +1044,10 @@ function Rail(props) {
1041
1044
  const [warming, setWarming] = createSignal(false)
1042
1045
  let warmToken = 0
1043
1046
  let warmedFor = ""
1047
+ // Set when a prewarm gave up without reaching the oldest turn (the host never
1048
+ // engaged). The auto-resume effect must not retry it in a loop; an explicit
1049
+ // hover or a fresh full load still may.
1050
+ let warmBlockedFor = ""
1044
1051
 
1045
1052
  // The topmost visible row plus its offset, so the viewport can be restored.
1046
1053
  const viewAnchor = () => {
@@ -1100,24 +1107,61 @@ function Rail(props) {
1100
1107
  setWarming(true)
1101
1108
  try {
1102
1109
  navTo(path, oldest.id)
1103
- const deadline = Date.now() + 120000
1110
+ // The host's seek machine starts inactive and DROPS a seek issued before
1111
+ // the timeline is ready - exactly the situation on session open. So don't
1112
+ // wait blindly for the oldest row: watch for real paging progress, and if
1113
+ // the host goes silent, bounce the hash to re-engage it once it is ready.
1114
+ // If it never engages, bail fast (without marking warm) so clicks fall
1115
+ // back to a direct jump instead of all showing "预热中 · 稍等一下" for 2min.
1116
+ const deadline = Date.now() + 60000
1117
+ let lastSig = rowSignature()
1118
+ let lastChange = Date.now()
1119
+ let kicks = 0
1104
1120
  const keep = setInterval(() => restoreView(anchor), 150)
1105
1121
  try {
1106
1122
  while (Date.now() < deadline && token === warmToken) {
1107
1123
  if (findRow(oldest.id)) break
1108
1124
  await sleep(250)
1125
+ if (token !== warmToken) break
1126
+ const now = Date.now()
1127
+ const sig = rowSignature()
1128
+ if (sig !== lastSig) {
1129
+ // Real progress: the host is paging. Keep waiting, reset the kick budget.
1130
+ lastSig = sig
1131
+ lastChange = now
1132
+ kicks = 0
1133
+ } else if (now - lastChange > 5000) {
1134
+ if (kicks >= 2) break // host never engaged: give up, stay un-warm
1135
+ // Re-engage: a repeated identical hash is ignored by the router, so
1136
+ // bounce through a mounted turn first, then back to the oldest.
1137
+ const other = oldestLoadedTurn()
1138
+ if (other && other.id !== oldest.id) {
1139
+ navTo(path, other.id)
1140
+ await sleep(JUMP_TOGGLE_DWELL_MS)
1141
+ if (token !== warmToken) break
1142
+ }
1143
+ navTo(path, oldest.id)
1144
+ lastChange = Date.now()
1145
+ kicks++
1146
+ }
1109
1147
  }
1110
1148
  } finally {
1111
1149
  clearInterval(keep)
1112
1150
  }
1113
1151
  if (token !== warmToken) return
1152
+ const done = !!findRow(oldest.id)
1114
1153
  // The host scrolled to the oldest message; put the viewport back.
1115
1154
  restoreView(anchor)
1116
1155
  if (wasAtBottom) {
1117
1156
  const live = scroller()
1118
1157
  if (live) live.scrollTop = live.scrollHeight
1119
1158
  }
1120
- warmedFor = sid
1159
+ if (done) {
1160
+ warmedFor = sid
1161
+ } else {
1162
+ // Never claim the history is ready when the oldest turn never mounted.
1163
+ warmBlockedFor = sid
1164
+ }
1121
1165
  // Silent on success: prewarming is an implementation detail.
1122
1166
  } finally {
1123
1167
  if (token === warmToken) setWarming(false)
@@ -1145,13 +1189,15 @@ function Rail(props) {
1145
1189
  // history still gets warmed for the next click. The initial start happens in
1146
1190
  // load()'s finally (once the full turn list is in); this only fires when the
1147
1191
  // warming signal flips back to false while the session is not fully warm.
1192
+ // A prewarm that gave up (warmBlockedFor) is NOT retried here, otherwise a
1193
+ // host that never engages would loop forever and keep every click on "稍等".
1148
1194
  createEffect(() => {
1149
1195
  const sid = sessionID()
1150
1196
  const count = turns().length
1151
1197
  const hot = warming()
1152
- if (!sid || count < 15 || hot || warmedFor === sid) return
1198
+ if (!sid || count < 15 || hot || warmedFor === sid || warmBlockedFor === sid) return
1153
1199
  if (loadingActive || loadedFor !== sid) return
1154
- const timer = setTimeout(() => maybePrewarm(), 800)
1200
+ const timer = setTimeout(() => maybePrewarm(), 1200)
1155
1201
  onCleanup(() => clearTimeout(timer))
1156
1202
  })
1157
1203