opencode-session-turns 0.1.7 → 0.1.9

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 a=require("solid-js/web"),R=require("@opencode/plugin/gui"),i=require("solid-js");var Ot=a.template("<div class=sr-status>"),Nt=a.template('<span class="sr-title sr-progress">跳转中'),Ft=a.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),Ht=a.template("<span class=sr-title>会话轮次"),Ut=a.template("<div class=sr-note>"),zt=a.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 tt="v13",nt="message-",Et="session-turns:diag",st="session-turns:open",Gt=40,Wt=200,Vt=50,Kt=30,j=45e3,Yt=12e3,Zt=5e3,Xt=9e3,Qt=220,te=800;let At=0,ot="",at="";const _t=o=>{const r=String(o?.text??"").split(`
2
- `).map(g=>g.trim()).find(g=>g.length>0);if(r)return r;const d=o?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},Ct=o=>String(o).padStart(2,"0"),ee=o=>{const r=new Date(o??0);return`${Ct(r.getHours())}:${Ct(r.getMinutes())}`},Tt=o=>new Promise(r=>setTimeout(r,o)),it=()=>document.querySelector('[data-slot="session-timeline-scroll"] > .scroll-view__viewport')??document.querySelector('[data-slot="session-timeline-scroll"] [data-scrollable]')??(()=>{let r=document.querySelector("[data-message-id]")?.parentElement;for(;r;){const d=getComputedStyle(r);if(/(auto|scroll)/.test(d.overflowY)&&r.scrollHeight>r.clientHeight+1)return r;r=r.parentElement}})(),T=o=>document.getElementById(`${nt}${o}`)??[...document.querySelectorAll("[data-message-id]")].find(r=>r.getAttribute("data-message-id")===o),et=o=>{const r=it(),d=[...document.querySelectorAll("[data-message-id]")],g=d[0]?.getAttribute("data-message-id")??"",S=at?` · 目标 ${T(at)?"在":"不在"} DOM`:"",A=g?` · 行ID ${g.slice(0,20)}`:"";let y="(空)";try{y=o.router.path()||"(空)"}catch{}return`轮次 ${At} · 容器 ${r?"OK":"缺失"} · DOM ${d.length} 行${S}${A} · 路由 ${y} · hash ${window.location.hash||"(空)"}${ot?` · ${ot}`:""}`},rt=(o,r)=>{const d=o.querySelector("[data-session-title]"),g=o.getBoundingClientRect(),S=d instanceof HTMLElement?d.getBoundingClientRect().bottom:g.top,y=r.getBoundingClientRect().top-(S+24);Math.abs(y)>2&&(o.scrollTop+=y)},re=o=>{setTimeout(()=>{if(!o.isConnected)return;const r=it();if(!r)return;const d=r.querySelector("[data-session-title]"),g=d instanceof HTMLElement?d.getBoundingClientRect().bottom:r.getBoundingClientRect().top,S=o.getBoundingClientRect().top;S<g+4&&(r.scrollTop+=S-(g+24))},300)},ne=o=>{let r="";try{r=o.router.path()||""}catch{}return r.startsWith("/")?r:""},se=o=>{const r=document.createElement("a");r.setAttribute("href",o),r.setAttribute("replace",""),r.setAttribute("noscroll",""),r.style.display="none",document.body.appendChild(r);try{r.dispatchEvent(new MouseEvent("click",{bubbles:!0,cancelable:!0,button:0,view:window}))}finally{r.remove()}},oe=`
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("solid-js/web"),q=require("@opencode/plugin/gui"),i=require("solid-js");var zt=a.template("<div class=sr-status>"),Ft=a.template('<span class="sr-title sr-progress">跳转中'),Ut=a.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),Gt=a.template("<span class=sr-title>会话轮次"),Ht=a.template("<div class=sr-note>"),Wt=a.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 et="v13",ot="message-",Ct="session-turns:diag",at="session-turns:open",Vt=40,Kt=200,Yt=50,Zt=30,j=45e3,Xt=12e3,Qt=5e3,te=9e3,ee=220,ne=800;let Lt=0,it="",lt="",ct="";const At=s=>{const n=String(s?.text??"").split(`
2
+ `).map(p=>p.trim()).find(p=>p.length>0);if(n)return n;const d=s?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},Tt=s=>String(s).padStart(2,"0"),re=s=>{const n=new Date(s??0);return`${Tt(n.getHours())}:${Tt(n.getMinutes())}`},Mt=s=>new Promise(n=>setTimeout(n,s)),dt=()=>document.querySelector('[data-slot="session-timeline-scroll"] > .scroll-view__viewport')??document.querySelector('[data-slot="session-timeline-scroll"] [data-scrollable]')??(()=>{let n=document.querySelector("[data-message-id]")?.parentElement;for(;n;){const d=getComputedStyle(n);if(/(auto|scroll)/.test(d.overflowY)&&n.scrollHeight>n.clientHeight+1)return n;n=n.parentElement}})(),T=s=>document.getElementById(`${ot}${s}`)??[...document.querySelectorAll("[data-message-id]")].find(n=>n.getAttribute("data-message-id")===s),nt=s=>{const n=dt(),d=[...document.querySelectorAll("[data-message-id]")],p=d[0]?.getAttribute("data-message-id")??"",$=ct?` · 目标 ${T(ct)?"在":"不在"} DOM`:"",S=p?` · 行ID ${p.slice(0,20)}`:"";let M="(空)";try{M=s.router.path()||"(空)"}catch{}return`轮次 ${Lt} · 容器 ${n?"OK":"缺失"} · DOM ${d.length} 行${$}${S} · 路由 ${M} · hash ${window.location.hash||"(空)"}${lt?` · ${lt}`:""}${it?` · ${it}`:""}`},se=s=>{const n=s.getBoundingClientRect().top;let d=n;for(const p of s.querySelectorAll(".sticky, [data-session-title]")){if(!(p instanceof HTMLElement)||getComputedStyle(p).position!=="sticky"&&!p.hasAttribute("data-session-title"))continue;const S=p.getBoundingClientRect();S.height!==0&&(S.top>n+4||(d=Math.max(d,S.bottom)))}return d},rt=s=>{try{const n=dt();if(!n||!s.isConnected)return;const d=s.querySelector('[data-slot="user-message-text"]')??s,p=se(n),$=d.getBoundingClientRect().top-(p+18);Math.abs($)>2&&(n.scrollTop+=$),lt=`inset ${Math.round(p)} · delta ${Math.round($)}`}catch{}},st=s=>{rt(s),setTimeout(()=>rt(s),450),setTimeout(()=>rt(s),1e3)},oe=s=>{let n="";try{n=s.router.path()||""}catch{}return n.startsWith("/")?n:""},ae=s=>{const n=document.createElement("a");n.setAttribute("href",s),n.setAttribute("replace",""),n.setAttribute("noscroll",""),n.style.display="none",document.body.appendChild(n);try{n.dispatchEvent(new MouseEvent("click",{bubbles:!0,cancelable:!0,button:0,view:window}))}finally{n.remove()}},ie=`
3
3
  .sr-rail {
4
4
  position: fixed;
5
5
  top: 50%;
@@ -94,6 +94,9 @@
94
94
  color: inherit;
95
95
  font: inherit;
96
96
  border-radius: 4px;
97
+ /* long sessions: skip layout/paint for rows outside the panel viewport */
98
+ content-visibility: auto;
99
+ contain-intrinsic-size: 0 26px;
97
100
  }
98
101
  /* expanded: dash flips to a left-side indicator, content reads left-to-right */
99
102
  .sr-rail[data-open="true"] .sr-turn {
@@ -215,4 +218,4 @@
215
218
  white-space: pre-wrap;
216
219
  max-width: min(720px, 90vw);
217
220
  }
218
- `;function ae(o){const r=R.usePlugin(),[d,g]=i.createSignal([]),[S,A]=i.createSignal(""),[y,C]=i.createSignal(!1),[Lt,Mt]=i.createSignal(""),[lt,ct]=i.createSignal(""),[k,q]=i.createSignal(!1),[B,L]=i.createSignal(0),[z,M]=i.createSignal(0),[le,J]=i.createSignal(0),[G,O]=i.createSignal(""),$=new Map;let W=0,N=!1,F=!1,V="",I;const E=()=>o.session()?.id,K=()=>{const t=[...$.values()].sort((n,l)=>n.created-l.created),e=i.untrack(d);e.length===t.length&&e.every((n,l)=>n===t[l])||(g(t),At=t.length)},dt=t=>{const e=$.get(t.id);e&&e.created===t.time&&e.text===t.text||$.set(t.id,{id:t.id,created:t.time,text:t.text})},It=async(t,e,n)=>{const l=n?{sessionID:t,type:"user",limit:e,cursor:n}:{sessionID:t,type:"user",limit:e,order:"desc"};return await r.client.message.list(l)},Y=async(t,e)=>{const n=++W;N=!0;let l="";try{let s;const c=e==="full"?Gt:1,u=e==="full"?Wt:Vt;let m=e!=="full";for(let v=0;v<c;v++){let h;try{h=await It(t,u,s)}catch(p){l=String(p?.message??p);break}if(n!==W)return;m||($.clear(),m=!0);let f=0;for(const p of h?.data??[])$.has(p.id)||(dt({id:p.id,time:p.time?.created??0,text:_t(p)}),f++);K();const x=h?.cursor?.next;if(!x||f===0)break;s=x}}finally{N=!1,V=t,ot=l,Mt($.size===0?l||"暂无用户消息":""),F&&(F=!1,ut())}},ut=()=>{I||(I=setTimeout(()=>{I=void 0;const t=i.untrack(E);t&&Y(t,"light")},300))},pt=t=>{if(!(!t||t!==i.untrack(E))){if(N){F=!0;return}ut()}},Dt=t=>{const e=t?.type;if(e==="message.updated"){const n=t?.data??{},l=n.info;l?.role==="user"&&l?.id&&(dt({id:l.id,time:l.time?.created??0,text:_t(l)}),K()),pt(n.sessionID)}else if(e==="message.removed"){const n=t?.data??{};n.messageID&&$.delete(n.messageID)&&K(),pt(n.sessionID)}else if(e==="location.shutdown"){const n=i.untrack(E);n&&V===n&&Y(n,"full")}};i.onMount(()=>{let t=!1,e=0,n;(async()=>{for(;!t;){const c=r.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!t;){const u=await c.next();if(u.done)break;e=0,Dt(u.value)}}catch{}try{await c.return?.()}catch{}if(t)break;e++,await Tt(Math.min(2e3*e,15e3))}})(),i.onCleanup(()=>{t=!0,clearTimeout(n),I&&clearTimeout(I)})}),i.createEffect(()=>{const t=E();t&&i.untrack(()=>{V!==t&&(W++,N=!1,F=!1,$.clear(),g([]),A(""),X="",Y(t,"full"))})});let H;const D=()=>(H?.isConnected||(H=it()),H),Pt=()=>{const t=D();if(!t)return;const e=t.querySelectorAll("[data-message-id]");if(e.length===0)return;const n=t.getBoundingClientRect().top+40;for(const l of e)if(l.getBoundingClientRect().bottom>=n){const s=l.getAttribute("data-message-id");s&&A(s);break}};let Z=!1;const ft=()=>{Z||(Z=!0,requestAnimationFrame(()=>{Z=!1,Pt()}))};i.onMount(()=>{const t=()=>ft();document.addEventListener("scroll",t,!0);const e=setInterval(ft,1500);i.onCleanup(()=>{document.removeEventListener("scroll",t,!0),clearInterval(e)})});let gt=0;const w=(t,e=2600)=>{const n=++gt;ct(t),setTimeout(()=>{n===gt&&ct("")},e)};i.onMount(()=>{const t=n=>{if(n.key==="Escape"){if(i.untrack(k)){vt(),n.stopPropagation();return}i.untrack(y)&&(C(!1),n.stopPropagation())}},e=n=>{if(!i.untrack(y))return;(n.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||C(!1)};window.addEventListener("keydown",t,!0),document.addEventListener("pointerdown",e,!0),i.onCleanup(()=>{window.removeEventListener("keydown",t,!0),document.removeEventListener("pointerdown",e,!0)})}),i.onMount(()=>{const t=()=>w(`${tt} · ${et(r)}`),e=()=>C(n=>!n);window.addEventListener(Et,t),window.addEventListener(st,e),i.onCleanup(()=>{window.removeEventListener(Et,t),window.removeEventListener(st,e)}),w(`会话轮次 ${tt} 已加载`)});let X="",_=0,ht=0,mt="";const Q=t=>{try{const e=t.querySelector('[data-slot="user-message-text"]')??t.querySelector('[data-slot="user-message-body"]')??t;e.classList.remove("sr-flash-ring"),e.offsetWidth,e.classList.add("sr-flash-ring"),setTimeout(()=>e.classList.remove("sr-flash-ring"),1900)}catch{}},jt=()=>{for(const t of d())if(T(t.id))return t},bt=(t,e)=>{se(`${t}#${nt}${e}`),X=e},wt=()=>{const e=D()?.querySelectorAll("[data-message-id]")??[];if(e.length===0)return"0";const n=e[0].getAttribute("data-message-id")??"",l=e[e.length-1].getAttribute("data-message-id")??"";return`${e.length}:${n}:${l}`},Rt=(t,e,n,l)=>new Promise(s=>{let c=!1,u,m,v;const h=Date.now();let x=wt(),p=h,b=0;const P=U=>{if(!c){c=!0;try{u?.disconnect()}catch{}clearInterval(m),clearTimeout(v),s(U??null)}},xt=U=>{if(e!==_)return P(null);const kt=U?T(t):document.getElementById(`${nt}${t}`);if(kt)return P(kt);if(!U)return;const St=wt(),$t=Date.now();if(St!==x){x=St,p=$t,b++,l?.(b);return}const Jt=b>0?Zt:Xt;$t-p>Jt&&P(null)},yt=T(t);if(yt)return P(yt);u=new MutationObserver(()=>xt(!1)),u.observe(document.documentElement,{childList:!0,subtree:!0}),m=setInterval(()=>xt(!0),120),v=setTimeout(()=>P(T(t)),Math.max(400,n))}),vt=()=>{_++,q(!1),L(0),M(0),J(0),O(""),w("已取消跳转")},qt=async t=>{if(k()&&G()===t.id){w(`正在跳转 · 已加载 ${B()} 页 · ${z()}s`,j);return}const e=++_;q(!1),L(0),M(0),J(0),O(""),at=t.id;const n=Date.now();if(t.id===mt&&n-ht<te){const h=T(t.id),f=D();if(h&&f){rt(f,h),Q(h);return}}ht=n,mt=t.id;const l=D(),s=T(t.id);if(s&&l){rt(l,s),Q(s),w("已定位");return}const c=ne(r);if(!c){w(`无法读取当前路由,无法定位 · ${et(r)}`);return}const u=E();q(!0),O(t.id),L(0),M(0),C(!0),w("正在定位…",j);const m=Date.now(),v=setInterval(()=>{M(Math.round((Date.now()-m)/1e3))},500);try{const h=Date.now()+j;for(let f=0;f<Kt&&Date.now()<h;f++){if(e!==_||E()!==u)return;if(J(f+1),X===t.id){const b=jt()??d()[0];b&&(bt(c,b.id),await Tt(Qt))}if(e!==_||E()!==u)return;bt(c,t.id),w(`跳转中 · 已加载 ${B()} 页`,j);const x=Math.min(Yt,h-Date.now()),p=await Rt(t.id,e,x,b=>{L(b),w(`跳转中 · 已加载 ${b} 页`,j)});if(e!==_||E()!==u)return;if(p){const b=D()??l;b&&rt(b,p),Q(p),w("已定位"),re(p);return}}e===_&&w(`未能定位 · ${et(r)}`)}finally{clearInterval(v),e===_&&(q(!1),L(0),M(0),J(0),O(""))}};i.createEffect(()=>{k()&&G()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Bt=[a.createComponent(i.Show,{get when(){return lt()},get children(){var t=Ot();return a.insert(t,lt),t}}),(()=>{var t=Ft(),e=t.firstChild,n=e.firstChild,l=e.nextSibling;return t.addEventListener("mouseleave",()=>{k()||C(!1)}),t.addEventListener("mouseenter",()=>C(!0)),a.insert(e,a.createComponent(i.Show,{get when(){return k()},get fallback(){return(()=>{var s=Ht();return s.firstChild,a.insert(s,(()=>{var c=a.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),s})()},get children(){var s=Nt();return s.firstChild,a.insert(s,(()=>{var c=a.memo(()=>B()>0);return()=>c()?` · 已加载 ${B()} 页`:""})(),null),a.insert(s,(()=>{var c=a.memo(()=>z()>0);return()=>c()?` · ${z()}s`:""})(),null),s}}),n),n.$$click=()=>{k()?vt():C(!1)},a.insert(l,a.createComponent(i.Show,{get when(){return d().length>0},get fallback(){return(()=>{var s=Ut();return a.insert(s,()=>Lt()||`… ${tt}`),s})()},get children(){return a.createComponent(i.For,{get each(){return d()},children:(s,c)=>(()=>{var u=zt(),m=u.firstChild,v=m.firstChild;v.firstChild;var h=v.nextSibling;return u.$$click=()=>{qt(s)},a.insert(v,()=>c()+1,null),a.insert(h,()=>ee(s.created)),a.insert(m,()=>s.text,null),a.effect(f=>{var x=S()===s.id?"true":"false",p=k()&&G()===s.id?"true":"false";return x!==f.e&&a.setAttribute(u,"data-active",f.e=x),p!==f.t&&a.setAttribute(u,"data-busy",f.t=p),f},{e:void 0,t:void 0}),u})()})}})),a.effect(s=>{var c=y()?"true":"false",u=k()?"true":"false",m=k()?"取消跳转 (Esc)":"关闭 (Esc)";return c!==s.e&&a.setAttribute(t,"data-open",s.e=c),u!==s.t&&a.setAttribute(t,"data-jumping",s.t=u),m!==s.a&&a.setAttribute(n,"title",s.a=m),s},{e:void 0,t:void 0,a:void 0}),t})()];return a.createComponent(a.Portal,{get mount(){return document.body},children:Bt})}const ie=R.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(o){o.add(R.Style,oe),o.add(R.Slot,{at:"window.bottom",render:()=>a.createComponent(i.Show,{get when(){return o.sessions.current()},get children(){return a.createComponent(ae,{session:()=>o.sessions.current()})}})}),o.add(R.Command,()=>({id:"jump",title:o.t("command.jump"),description:o.t("command.jump.description"),group:o.t("command.jump"),section:"navigation",bind:"mod+alt+r",slash:{name:"turns"},run:()=>{window.dispatchEvent(new Event(st))}}))}});a.delegateEvents(["click"]);exports.default=ie;
221
+ `;function le(s){const n=q.usePlugin(),[d,p]=i.createSignal([]),[$,S]=i.createSignal(""),[M,A]=i.createSignal(!1),[It,Dt]=i.createSignal(""),[ut,pt]=i.createSignal(""),[w,R]=i.createSignal(!1),[G,L]=i.createSignal(0),[H,I]=i.createSignal(0),[de,J]=i.createSignal(0),[W,B]=i.createSignal(""),E=new Map;let V=0,O=!1,N=!1,K="",D;const _=()=>s.session()?.id,Y=()=>{const t=[...E.values()].sort((r,l)=>r.created-l.created),e=i.untrack(d);e.length===t.length&&e.every((r,l)=>r===t[l])||(p(t),Lt=t.length)},ft=t=>{const e=E.get(t.id);e&&e.created===t.time&&e.text===t.text||E.set(t.id,{id:t.id,created:t.time,text:t.text})},Pt=async(t,e,r)=>{const l=r?{sessionID:t,type:"user",limit:e,cursor:r}:{sessionID:t,type:"user",limit:e,order:"desc"};return await n.client.message.list(l)},Z=async(t,e)=>{const r=++V;O=!0;let l="";try{let o;const c=e==="full"?Vt:1,u=e==="full"?Kt:Yt;let m=e!=="full";for(let v=0;v<c;v++){let b;try{b=await Pt(t,u,o)}catch(h){l=String(h?.message??h);break}if(r!==V)return;m||(E.clear(),m=!0);let f=0;for(const h of b?.data??[])E.has(h.id)||(ft({id:h.id,time:h.time?.created??0,text:At(h)}),f++);Y();const g=b?.cursor?.next;if(!g||f===0)break;o=g}}finally{O=!1,K=t,it=l,Dt(E.size===0?l||"暂无用户消息":""),N&&(N=!1,ht())}},ht=()=>{D||(D=setTimeout(()=>{D=void 0;const t=i.untrack(_);t&&Z(t,"light")},300))},gt=t=>{if(!(!t||t!==i.untrack(_))){if(O){N=!0;return}ht()}},jt=t=>{const e=t?.type;if(e==="message.updated"){const r=t?.data??{},l=r.info;l?.role==="user"&&l?.id&&(ft({id:l.id,time:l.time?.created??0,text:At(l)}),Y()),gt(r.sessionID)}else if(e==="message.removed"){const r=t?.data??{};r.messageID&&E.delete(r.messageID)&&Y(),gt(r.sessionID)}else if(e==="location.shutdown"){const r=i.untrack(_);r&&K===r&&Z(r,"full")}};i.onMount(()=>{let t=!1,e=0,r;(async()=>{for(;!t;){const c=n.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!t;){const u=await c.next();if(u.done)break;e=0,jt(u.value)}}catch{}try{await c.return?.()}catch{}if(t)break;e++,await Mt(Math.min(2e3*e,15e3))}})(),i.onCleanup(()=>{t=!0,clearTimeout(r),D&&clearTimeout(D)})}),i.createEffect(()=>{const t=_();t&&i.untrack(()=>{K!==t&&(V++,O=!1,N=!1,E.clear(),p([]),S(""),Q="",Z(t,"full"))})});let z;const F=()=>(z?.isConnected||(z=dt()),z),qt=()=>{const t=F();if(!t)return;const e=t.querySelectorAll("[data-message-id]");if(e.length===0)return;const r=t.getBoundingClientRect().top+40;for(const l of e)if(l.getBoundingClientRect().bottom>=r){const o=l.getAttribute("data-message-id");o&&S(o);break}};let X=!1;const mt=()=>{X||(X=!0,requestAnimationFrame(()=>{X=!1,qt()}))};i.onMount(()=>{const t=()=>mt();document.addEventListener("scroll",t,!0);const e=setInterval(mt,1500);i.onCleanup(()=>{document.removeEventListener("scroll",t,!0),clearInterval(e)})});let bt=0;const x=(t,e=2600)=>{const r=++bt;pt(t),setTimeout(()=>{r===bt&&pt("")},e)};i.onMount(()=>{const t=r=>{if(r.key==="Escape"){if(i.untrack(w)){kt(),r.stopPropagation();return}i.untrack(M)&&(A(!1),r.stopPropagation())}},e=r=>{if(!i.untrack(M))return;(r.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||A(!1)};window.addEventListener("keydown",t,!0),document.addEventListener("pointerdown",e,!0),i.onCleanup(()=>{window.removeEventListener("keydown",t,!0),document.removeEventListener("pointerdown",e,!0)})}),i.onMount(()=>{const t=()=>x(`${et} · ${nt(n)}`),e=()=>A(r=>!r);window.addEventListener(Ct,t),window.addEventListener(at,e),i.onCleanup(()=>{window.removeEventListener(Ct,t),window.removeEventListener(at,e)}),x(`会话轮次 ${et} 已加载`)});let Q="",C=0,xt=0,vt="";const tt=t=>{try{const e=t.querySelector('[data-slot="user-message-text"]')??t.querySelector('[data-slot="user-message-body"]')??t;e.classList.remove("sr-flash-ring"),e.offsetWidth,e.classList.add("sr-flash-ring"),setTimeout(()=>e.classList.remove("sr-flash-ring"),1900)}catch{}},Rt=()=>{for(const t of d())if(T(t.id))return t},wt=(t,e)=>{ae(`${t}#${ot}${e}`),Q=e},yt=()=>{const e=F()?.querySelectorAll("[data-message-id]")??[];if(e.length===0)return"0";const r=e[0].getAttribute("data-message-id")??"",l=e[e.length-1].getAttribute("data-message-id")??"";return`${e.length}:${r}:${l}`},Jt=(t,e,r,l)=>new Promise(o=>{let c=!1,u,m,v;const b=Date.now();let g=yt(),h=b,P=0;const y=U=>{if(!c){c=!0;try{u?.disconnect()}catch{}clearInterval(m),clearTimeout(v),o(U??null)}},k=U=>{if(e!==C)return y(null);const St=U?T(t):document.getElementById(`${ot}${t}`);if(St)return y(St);if(!U)return;const Et=yt(),_t=Date.now();if(Et!==g){g=Et,h=_t,P++,l?.(P);return}const Nt=P>0?Qt:te;_t-h>Nt&&y(null)},$t=T(t);if($t)return y($t);u=new MutationObserver(()=>k(!1)),u.observe(document.documentElement,{childList:!0,subtree:!0}),m=setInterval(()=>k(!0),120),v=setTimeout(()=>y(T(t)),Math.max(400,r))}),kt=()=>{C++,R(!1),L(0),I(0),J(0),B(""),x("已取消跳转")},Bt=async t=>{if(w()&&W()===t.id){x(`正在跳转 · 已加载 ${G()} 页 · ${H()}s`,j);return}const e=++C;R(!1),L(0),I(0),J(0),B(""),ct=t.id;const r=Date.now();if(t.id===vt&&r-xt<ne){const f=T(t.id),g=F();if(f&&g){st(f),tt(f);return}}xt=r,vt=t.id;const l=F(),o=T(t.id);if(o&&l){st(o),tt(o),x("已定位");return}const c=oe(n);if(!c){x(`无法读取当前路由,无法定位 · ${nt(n)}`);return}const u=_();R(!0),B(t.id),L(0),I(0),A(!0),x("正在定位…",j);const m=Date.now(),v=setInterval(()=>{I(Math.round((Date.now()-m)/1e3))},500);let b=0;try{const f=Date.now()+j;for(let g=0;g<Zt&&Date.now()<f;g++){if(e!==C||_()!==u)return;if(J(g+1),Q===t.id){const k=Rt()??d()[0];k&&(wt(c,k.id),await Mt(ee))}if(e!==C||_()!==u)return;wt(c,t.id),x(`跳转中 · 已加载 ${b} 页`,j);let h=0;const P=Math.min(Xt,f-Date.now()),y=await Jt(t.id,e,P,k=>{h=k,L(b+k),x(`跳转中 · 已加载 ${b+k} 页`,j)});if(b+=h,e!==C||_()!==u)return;if(y){st(y),tt(y),x("已定位");return}}e===C&&x(`未能定位 · ${nt(n)}`)}finally{clearInterval(v),e===C&&(R(!1),L(0),I(0),J(0),B(""))}};i.createEffect(()=>{w()&&W()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Ot=[a.createComponent(i.Show,{get when(){return ut()},get children(){var t=zt();return a.insert(t,ut),t}}),(()=>{var t=Ut(),e=t.firstChild,r=e.firstChild,l=e.nextSibling;return t.addEventListener("mouseleave",()=>{w()||A(!1)}),t.addEventListener("mouseenter",()=>A(!0)),a.insert(e,a.createComponent(i.Show,{get when(){return w()},get fallback(){return(()=>{var o=Gt();return o.firstChild,a.insert(o,(()=>{var c=a.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),o})()},get children(){var o=Ft();return o.firstChild,a.insert(o,(()=>{var c=a.memo(()=>G()>0);return()=>c()?` · 已加载 ${G()} 页`:""})(),null),a.insert(o,(()=>{var c=a.memo(()=>H()>0);return()=>c()?` · ${H()}s`:""})(),null),o}}),r),r.$$click=()=>{w()?kt():A(!1)},a.insert(l,a.createComponent(i.Show,{get when(){return d().length>0},get fallback(){return(()=>{var o=Ht();return a.insert(o,()=>It()||`… ${et}`),o})()},get children(){return a.createComponent(i.For,{get each(){return d()},children:(o,c)=>(()=>{var u=Wt(),m=u.firstChild,v=m.firstChild;v.firstChild;var b=v.nextSibling;return u.$$click=()=>{Bt(o)},a.insert(v,()=>c()+1,null),a.insert(b,()=>re(o.created)),a.insert(m,()=>o.text,null),a.effect(f=>{var g=$()===o.id?"true":"false",h=w()&&W()===o.id?"true":"false";return g!==f.e&&a.setAttribute(u,"data-active",f.e=g),h!==f.t&&a.setAttribute(u,"data-busy",f.t=h),f},{e:void 0,t:void 0}),u})()})}})),a.effect(o=>{var c=M()?"true":"false",u=w()?"true":"false",m=w()?"取消跳转 (Esc)":"关闭 (Esc)";return c!==o.e&&a.setAttribute(t,"data-open",o.e=c),u!==o.t&&a.setAttribute(t,"data-jumping",o.t=u),m!==o.a&&a.setAttribute(r,"title",o.a=m),o},{e:void 0,t:void 0,a:void 0}),t})()];return a.createComponent(a.Portal,{get mount(){return document.body},children:Ot})}const ce=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(s){s.add(q.Style,ie),s.add(q.Slot,{at:"window.bottom",render:()=>a.createComponent(i.Show,{get when(){return s.sessions.current()},get children(){return a.createComponent(le,{session:()=>s.sessions.current()})}})}),s.add(q.Command,()=>({id:"jump",title:s.t("command.jump"),description:s.t("command.jump.description"),group:s.t("command.jump"),section:"navigation",bind:"mod+alt+r",slash:{name:"turns"},run:()=>{window.dispatchEvent(new Event(at))}}))}});a.delegateEvents(["click"]);exports.default=ce;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-session-turns",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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
@@ -35,6 +35,7 @@ const SAME_TARGET_WINDOW_MS = 800
35
35
  // Filled by Rail.load()/jump() so the command palette can report diagnostics.
36
36
  let lastTurnCount = 0
37
37
  let lastNote = ""
38
+ let lastAlign = ""
38
39
  let lastJumpID = ""
39
40
 
40
41
  const firstLine = (message) => {
@@ -91,37 +92,50 @@ const diagnostics = (ctx) => {
91
92
  try {
92
93
  route = ctx.router.path() || "(空)"
93
94
  } catch {}
94
- return `轮次 ${lastTurnCount} · 容器 ${scroller ? "OK" : "缺失"} · DOM ${nodes.length} 行${target}${id} · 路由 ${route} · hash ${window.location.hash || "(空)"}${lastNote ? ` · ${lastNote}` : ""}`
95
+ return `轮次 ${lastTurnCount} · 容器 ${scroller ? "OK" : "缺失"} · DOM ${nodes.length} 行${target}${id} · 路由 ${route} · hash ${window.location.hash || "(空)"}${lastAlign ? ` · ${lastAlign}` : ""}${lastNote ? ` · ${lastNote}` : ""}`
95
96
  }
96
97
 
97
- const scrollToRow = (scroller, row) => {
98
- // Align by rects instead of scrollIntoView: the host puts the row top flush
99
- // against the sticky session title, but a 16px gradient fade sits right below
100
- // it and would cover the top of the bubble (and our outline). Leave 24px so
101
- // the bubble clears both the title and the fade.
102
- const title = scroller.querySelector("[data-session-title]")
103
- const scRect = scroller.getBoundingClientRect()
104
- const titleBottom =
105
- title instanceof HTMLElement ? title.getBoundingClientRect().bottom : scRect.top
106
- const rowTop = row.getBoundingClientRect().top
107
- const delta = rowTop - (titleBottom + 24)
108
- if (Math.abs(delta) > 2) scroller.scrollTop += delta
98
+ // The timeline's top is covered by the host's sticky header: the session title
99
+ // (z-30) plus a 16px gradient fade strip that sits right below it. The host
100
+ // parks a sought row flush against that band, which is why the bubble's top
101
+ // edge (and the highlight ring) used to be cut off.
102
+ //
103
+ // Instead of guessing a fixed offset, measure the band: every sticky element
104
+ // pinned to the top of the scrollport contributes its bottom edge.
105
+ const topInset = (scroller) => {
106
+ const scTop = scroller.getBoundingClientRect().top
107
+ let bottom = scTop
108
+ for (const el of scroller.querySelectorAll(".sticky, [data-session-title]")) {
109
+ if (!(el instanceof HTMLElement)) continue
110
+ const style = getComputedStyle(el)
111
+ if (style.position !== "sticky" && !el.hasAttribute("data-session-title")) continue
112
+ const rect = el.getBoundingClientRect()
113
+ if (rect.height === 0) continue
114
+ if (rect.top > scTop + 4) continue // not pinned to the top
115
+ bottom = Math.max(bottom, rect.bottom)
116
+ }
117
+ return bottom
109
118
  }
110
119
 
111
- // The host's own hash seek may finish after ours and re-align the row flush
112
- // under the sticky title (its 16px fade then covers the bubble's top edge).
113
- // Repair only that case: if the user already scrolled elsewhere, leave it be.
114
- const repairScroll = (row) => {
115
- setTimeout(() => {
116
- if (!row.isConnected) return
120
+ // Places the message bubble (not the row) clear of the header band.
121
+ const alignBubble = (row) => {
122
+ try {
117
123
  const sc = findScroller()
118
- if (!sc) return
119
- const title = sc.querySelector("[data-session-title]")
120
- const titleBottom =
121
- title instanceof HTMLElement ? title.getBoundingClientRect().bottom : sc.getBoundingClientRect().top
122
- const rowTop = row.getBoundingClientRect().top
123
- if (rowTop < titleBottom + 4) sc.scrollTop += rowTop - (titleBottom + 24)
124
- }, 300)
124
+ if (!sc || !row.isConnected) return
125
+ const bubble = row.querySelector('[data-slot="user-message-text"]') ?? row
126
+ const inset = topInset(sc)
127
+ const delta = bubble.getBoundingClientRect().top - (inset + 18)
128
+ if (Math.abs(delta) > 2) sc.scrollTop += delta
129
+ lastAlign = `inset ${Math.round(inset)} · delta ${Math.round(delta)}`
130
+ } catch {}
131
+ }
132
+
133
+ // The host's own seek may finish after ours (it can even scroll smoothly),
134
+ // re-aligning the row flush under the header. Re-align a couple of times.
135
+ const alignAfterLand = (row) => {
136
+ alignBubble(row)
137
+ setTimeout(() => alignBubble(row), 450)
138
+ setTimeout(() => alignBubble(row), 1000)
125
139
  }
126
140
 
127
141
  // The desktop app (2.2.6) routes through a memory history, so window.location is
@@ -263,6 +277,9 @@ const CSS = `
263
277
  color: inherit;
264
278
  font: inherit;
265
279
  border-radius: 4px;
280
+ /* long sessions: skip layout/paint for rows outside the panel viewport */
281
+ content-visibility: auto;
282
+ contain-intrinsic-size: 0 26px;
266
283
  }
267
284
  /* expanded: dash flips to a left-side indicator, content reads left-to-right */
268
285
  .sr-rail[data-open="true"] .sr-turn {
@@ -848,7 +865,7 @@ function Rail(props) {
848
865
  const row = findRow(turn.id)
849
866
  const sc = scroller()
850
867
  if (row && sc) {
851
- scrollToRow(sc, row)
868
+ alignAfterLand(row)
852
869
  pulse(row)
853
870
  return
854
871
  }
@@ -861,7 +878,7 @@ function Rail(props) {
861
878
 
862
879
  // Fast path: the row is mounted, scroll locally.
863
880
  if (row && sc) {
864
- scrollToRow(sc, row)
881
+ alignAfterLand(row)
865
882
  pulse(row)
866
883
  flash("已定位")
867
884
  return
@@ -897,6 +914,9 @@ function Rail(props) {
897
914
  const elapsedTimer = setInterval(() => {
898
915
  setJumpElapsed(Math.round((Date.now() - startedAt) / 1000))
899
916
  }, 500)
917
+ // Pages loaded so far, accumulated across chains (a new chain restarts
918
+ // the per-chain counter, the headline must not go backwards).
919
+ let chainBase = 0
900
920
  try {
901
921
  const deadline = Date.now() + JUMP_TOTAL_MS
902
922
  for (let chain = 0; chain < JUMP_ROUNDS && Date.now() < deadline; chain++) {
@@ -919,20 +939,21 @@ function Rail(props) {
919
939
  }
920
940
  if (token !== jumpToken || sessionID() !== sessionAtJump) return
921
941
  navTo(path, turn.id)
922
- flash(`跳转中 · 已加载 ${jumpPages()} 页`, JUMP_TOTAL_MS)
942
+ flash(`跳转中 · 已加载 ${chainBase} 页`, JUMP_TOTAL_MS)
923
943
 
944
+ let chainPages = 0
924
945
  const remaining = Math.min(JUMP_CHAIN_MS, deadline - Date.now())
925
946
  const landed = await waitForRow(turn.id, token, remaining, (pages) => {
926
- setJumpPages(pages)
927
- flash(`跳转中 · 已加载 ${pages} 页`, JUMP_TOTAL_MS)
947
+ chainPages = pages
948
+ setJumpPages(chainBase + pages)
949
+ flash(`跳转中 · 已加载 ${chainBase + pages} 页`, JUMP_TOTAL_MS)
928
950
  })
951
+ chainBase += chainPages
929
952
  if (token !== jumpToken || sessionID() !== sessionAtJump) return
930
953
  if (landed) {
931
- const live = scroller() ?? sc
932
- if (live) scrollToRow(live, landed)
954
+ alignAfterLand(landed)
933
955
  pulse(landed)
934
956
  flash("已定位")
935
- repairScroll(landed)
936
957
  return
937
958
  }
938
959
  // The host went idle without landing: it exhausted this chain. The