opencode-session-turns 0.1.4 → 0.1.6

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"),L=require("@opencode/plugin/gui"),i=require("solid-js");var Lt=a.template("<div class=sr-status>"),It=a.template('<span class="sr-title sr-progress">跳转中'),Mt=a.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),Pt=a.template("<span class=sr-title>会话轮次"),Dt=a.template("<div class=sr-note>"),jt=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 G="v12",wt="message-",ht="session-turns:diag",Y="session-turns:open",qt=40,Rt=200,Ot=50,Jt=120,V=45e3,Nt=4e3,zt=300,Ft=800;let xt=0,Z="",X="";const gt=o=>{const s=String(o?.text??"").split(`
2
- `).map(b=>b.trim()).find(b=>b.length>0);if(s)return s;const d=o?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},mt=o=>String(o).padStart(2,"0"),Ht=o=>{const s=new Date(o??0);return`${mt(s.getHours())}:${mt(s.getMinutes())}`},bt=o=>new Promise(s=>setTimeout(s,o)),vt=()=>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}})(),$=o=>document.getElementById(`${wt}${o}`)??[...document.querySelectorAll("[data-message-id]")].find(s=>s.getAttribute("data-message-id")===o),W=o=>{const s=vt(),d=[...document.querySelectorAll("[data-message-id]")],b=d[0]?.getAttribute("data-message-id")??"",R=X?` · 目标 ${$(X)?"在":"不在"} DOM`:"",I=b?` · 行ID ${b.slice(0,20)}`:"";let E="(空)";try{E=o.router.path()||"(空)"}catch{}return`轮次 ${xt} · 容器 ${s?"OK":"缺失"} · DOM ${d.length} 行${R}${I} · 路由 ${E} · hash ${window.location.hash||"(空)"}${Z?` · ${Z}`:""}`},K=(o,s)=>{s.scrollIntoView({block:"start"});const d=o.querySelector("[data-session-title]"),b=d instanceof HTMLElement?d.offsetHeight:0;b&&(o.scrollTop-=b+12)},Ut=o=>{let s="";try{s=o.router.path()||""}catch{}return s.startsWith("/")?s:""},Bt=o=>{const s=document.createElement("a");s.setAttribute("href",o),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()}},Gt=`
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("solid-js/web"),I=require("@opencode/plugin/gui"),i=require("solid-js");var Dt=a.template("<div class=sr-status>"),jt=a.template('<span class="sr-title sr-progress">跳转中'),Rt=a.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),qt=a.template("<span class=sr-title>会话轮次"),Bt=a.template("<div class=sr-note>"),Ot=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 Y="v13",Q="message-",vt="session-turns:diag",tt="session-turns:open",Jt=40,Nt=200,Ut=50,zt=120,O=45e3,Ft=3e3,Gt=300,Ht=220,Wt=800;let $t=0,et="",rt="";const yt=o=>{const r=String(o?.text??"").split(`
2
+ `).map(m=>m.trim()).find(m=>m.length>0);if(r)return r;const d=o?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},kt=o=>String(o).padStart(2,"0"),Vt=o=>{const r=new Date(o??0);return`${kt(r.getHours())}:${kt(r.getMinutes())}`},St=o=>new Promise(r=>setTimeout(r,o)),nt=()=>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}})(),_=o=>document.getElementById(`${Q}${o}`)??[...document.querySelectorAll("[data-message-id]")].find(r=>r.getAttribute("data-message-id")===o),Z=o=>{const r=nt(),d=[...document.querySelectorAll("[data-message-id]")],m=d[0]?.getAttribute("data-message-id")??"",k=rt?` · 目标 ${_(rt)?"在":"不在"} DOM`:"",C=m?` · 行ID ${m.slice(0,20)}`:"";let v="(空)";try{v=o.router.path()||"(空)"}catch{}return`轮次 ${$t} · 容器 ${r?"OK":"缺失"} · DOM ${d.length} 行${k}${C} · 路由 ${v} · hash ${window.location.hash||"(空)"}${et?` · ${et}`:""}`},X=(o,r)=>{const d=o.querySelector("[data-session-title]"),m=o.getBoundingClientRect(),k=d instanceof HTMLElement?d.getBoundingClientRect().bottom:m.top,v=r.getBoundingClientRect().top-(k+24);Math.abs(v)>2&&(o.scrollTop+=v)},Kt=o=>{setTimeout(()=>{if(!o.isConnected)return;const r=nt();if(!r)return;const d=r.querySelector("[data-session-title]"),m=d instanceof HTMLElement?d.getBoundingClientRect().bottom:r.getBoundingClientRect().top,k=o.getBoundingClientRect().top;k<m+4&&(r.scrollTop+=k-(m+24))},300)},Yt=o=>{let r="";try{r=o.router.path()||""}catch{}return r.startsWith("/")?r:""},Zt=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()}},Xt=`
3
3
  .sr-rail {
4
4
  position: fixed;
5
5
  top: 50%;
@@ -215,4 +215,4 @@
215
215
  white-space: pre-wrap;
216
216
  max-width: min(720px, 90vw);
217
217
  }
218
- `;function Vt(o){const s=L.usePlugin(),[d,b]=i.createSignal([]),[R,I]=i.createSignal(""),[E,S]=i.createSignal(!1),[yt,kt]=i.createSignal(""),[Q,tt]=i.createSignal(""),[v,M]=i.createSignal(!1),[et,T]=i.createSignal(0),[rt,P]=i.createSignal(""),y=new Map;let O=0,D=!1,j=!1,J="",A;const _=()=>o.session()?.id,N=()=>{const t=[...y.values()].sort((r,l)=>r.created-l.created),e=i.untrack(d);e.length===t.length&&e.every((r,l)=>r===t[l])||(b(t),xt=t.length)},st=t=>{const e=y.get(t.id);e&&e.created===t.time&&e.text===t.text||y.set(t.id,{id:t.id,created:t.time,text:t.text})},St=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 s.client.message.list(l)},z=async(t,e)=>{const r=++O;D=!0;let l="";try{let n;const c=e==="full"?qt:1,u=e==="full"?Rt:Ot;let p=e!=="full";for(let h=0;h<c;h++){let x;try{x=await St(t,u,n)}catch(m){l=String(m?.message??m);break}if(r!==O)return;p||(y.clear(),p=!0);let f=0;for(const m of x?.data??[])y.has(m.id)||(st({id:m.id,time:m.time?.created??0,text:gt(m)}),f++);N();const g=x?.cursor?.next;if(!g||f===0)break;n=g}}finally{D=!1,J=t,Z=l,kt(y.size===0?l||"暂无用户消息":""),j&&(j=!1,nt())}},nt=()=>{A||(A=setTimeout(()=>{A=void 0;const t=i.untrack(_);t&&z(t,"light")},300))},at=t=>{if(!(!t||t!==i.untrack(_))){if(D){j=!0;return}nt()}},$t=t=>{const e=t?.type;if(e==="message.updated"){const r=t?.data??{},l=r.info;l?.role==="user"&&l?.id&&(st({id:l.id,time:l.time?.created??0,text:gt(l)}),N()),at(r.sessionID)}else if(e==="message.removed"){const r=t?.data??{};r.messageID&&y.delete(r.messageID)&&N(),at(r.sessionID)}else if(e==="location.shutdown"){const r=i.untrack(_);r&&J===r&&z(r,"full")}};i.onMount(()=>{let t=!1,e=0,r;(async()=>{for(;!t;){const c=s.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!t;){const u=await c.next();if(u.done)break;e=0,$t(u.value)}}catch{}try{await c.return?.()}catch{}if(t)break;e++,await bt(Math.min(2e3*e,15e3))}})(),i.onCleanup(()=>{t=!0,clearTimeout(r),A&&clearTimeout(A)})}),i.createEffect(()=>{const t=_();t&&i.untrack(()=>{J!==t&&(O++,D=!1,j=!1,y.clear(),b([]),I(""),H="",z(t,"full"))})});let q;const C=()=>(q?.isConnected||(q=vt()),q),Et=()=>{const t=C();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 n=l.getAttribute("data-message-id");n&&I(n);break}};let F=!1;const ot=()=>{F||(F=!0,requestAnimationFrame(()=>{F=!1,Et()}))};i.onMount(()=>{const t=()=>ot();document.addEventListener("scroll",t,!0);const e=setInterval(ot,1500);i.onCleanup(()=>{document.removeEventListener("scroll",t,!0),clearInterval(e)})});let it=0;const w=(t,e=2600)=>{const r=++it;tt(t),setTimeout(()=>{r===it&&tt("")},e)};i.onMount(()=>{const t=r=>{if(r.key==="Escape"){if(i.untrack(v)){pt(),r.stopPropagation();return}i.untrack(E)&&(S(!1),r.stopPropagation())}},e=r=>{if(!i.untrack(E))return;(r.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||S(!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(`${G} · ${W(s)}`),e=()=>S(r=>!r);window.addEventListener(ht,t),window.addEventListener(Y,e),i.onCleanup(()=>{window.removeEventListener(ht,t),window.removeEventListener(Y,e)}),w(`会话轮次 ${G} 已加载`)});let H="",k=0,lt=0,ct="";const U=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{}},_t=()=>{for(const t of d())if($(t.id))return t},dt=(t,e)=>{Bt(`${t}#${wt}${e}`),H=e},ut=()=>{const e=C()?.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}`},Tt=(t,e,r)=>new Promise(l=>{let n=!1,c,u,p;const h=Date.now(),x=ut(),f=B=>{if(!n){n=!0;try{c?.disconnect()}catch{}clearInterval(u),clearTimeout(p),l(B??null)}},g=B=>{if(e!==k)return f(null);const ft=$(t);if(ft)return f(ft);B&&Date.now()-h>zt&&ut()!==x&&f(null)},m=$(t);if(m)return f(m);c=new MutationObserver(()=>g(!1)),c.observe(document.documentElement,{childList:!0,subtree:!0}),u=setInterval(()=>g(!0),120),p=setTimeout(()=>f($(t)),Math.max(400,r))}),pt=()=>{k++,M(!1),T(0),P(""),w("已取消跳转")},At=async t=>{const e=++k;M(!1),T(0),P(""),X=t.id;const r=Date.now();if(t.id===ct&&r-lt<Ft){const p=$(t.id),h=C();if(p&&h){K(h,p),U(p);return}}lt=r,ct=t.id;const l=C(),n=$(t.id);if(n&&l){K(l,n),U(n),w("已定位");return}const c=Ut(s);if(!c){w(`无法读取当前路由,无法定位 · ${W(s)}`);return}const u=_();M(!0),P(t.id),T(0),S(!0),w("正在定位…",V);try{const p=Date.now()+V;for(let h=0;h<Jt&&Date.now()<p;h++){if(e!==k||_()!==u)return;if(T(h+1),w(`跳转中 · 第 ${h+1} 页`,V),H===t.id){const g=_t()??d()[0];g&&(dt(c,g.id),await bt(120))}if(e!==k)return;dt(c,t.id);const x=Math.min(Nt,p-Date.now()),f=await Tt(t.id,e,x);if(e!==k)return;if(f){const g=C()??l;g&&K(g,f),U(f),w("已定位");return}}e===k&&w(`未能定位 · ${W(s)}`)}finally{e===k&&(M(!1),T(0),P(""))}};i.createEffect(()=>{v()&&rt()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Ct=[a.createComponent(i.Show,{get when(){return Q()},get children(){var t=Lt();return a.insert(t,Q),t}}),(()=>{var t=Mt(),e=t.firstChild,r=e.firstChild,l=e.nextSibling;return t.addEventListener("mouseleave",()=>{v()||S(!1)}),t.addEventListener("mouseenter",()=>S(!0)),a.insert(e,a.createComponent(i.Show,{get when(){return v()},get fallback(){return(()=>{var n=Pt();return n.firstChild,a.insert(n,(()=>{var c=a.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),n})()},get children(){var n=It();return n.firstChild,a.insert(n,(()=>{var c=a.memo(()=>et()>0);return()=>c()?` · 第 ${et()} 页`:""})(),null),n}}),r),r.$$click=()=>{v()?pt():S(!1)},a.insert(l,a.createComponent(i.Show,{get when(){return d().length>0},get fallback(){return(()=>{var n=Dt();return a.insert(n,()=>yt()||`… ${G}`),n})()},get children(){return a.createComponent(i.For,{get each(){return d()},children:(n,c)=>(()=>{var u=jt(),p=u.firstChild,h=p.firstChild;h.firstChild;var x=h.nextSibling;return u.$$click=()=>{At(n)},a.insert(h,()=>c()+1,null),a.insert(x,()=>Ht(n.created)),a.insert(p,()=>n.text,null),a.effect(f=>{var g=R()===n.id?"true":"false",m=v()&&rt()===n.id?"true":"false";return g!==f.e&&a.setAttribute(u,"data-active",f.e=g),m!==f.t&&a.setAttribute(u,"data-busy",f.t=m),f},{e:void 0,t:void 0}),u})()})}})),a.effect(n=>{var c=E()?"true":"false",u=v()?"true":"false",p=v()?"取消跳转 (Esc)":"关闭 (Esc)";return c!==n.e&&a.setAttribute(t,"data-open",n.e=c),u!==n.t&&a.setAttribute(t,"data-jumping",n.t=u),p!==n.a&&a.setAttribute(r,"title",n.a=p),n},{e:void 0,t:void 0,a:void 0}),t})()];return a.createComponent(a.Portal,{get mount(){return document.body},children:Ct})}const Wt=L.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(L.Style,Gt),o.add(L.Slot,{at:"window.bottom",render:()=>a.createComponent(i.Show,{get when(){return o.sessions.current()},get children(){return a.createComponent(Vt,{session:()=>o.sessions.current()})}})}),o.add(L.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(Y))}}))}});a.delegateEvents(["click"]);exports.default=Wt;
218
+ `;function Qt(o){const r=I.usePlugin(),[d,m]=i.createSignal([]),[k,C]=i.createSignal(""),[v,T]=i.createSignal(!1),[Et,Tt]=i.createSignal(""),[st,ot]=i.createSignal(""),[y,P]=i.createSignal(!1),[J,A]=i.createSignal(0),[N,D]=i.createSignal(""),S=new Map;let U=0,j=!1,R=!1,z="",L;const $=()=>o.session()?.id,F=()=>{const t=[...S.values()].sort((n,l)=>n.created-l.created),e=i.untrack(d);e.length===t.length&&e.every((n,l)=>n===t[l])||(m(t),$t=t.length)},at=t=>{const e=S.get(t.id);e&&e.created===t.time&&e.text===t.text||S.set(t.id,{id:t.id,created:t.time,text:t.text})},_t=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)},G=async(t,e)=>{const n=++U;j=!0;let l="";try{let s;const c=e==="full"?Jt:1,u=e==="full"?Nt:Ut;let p=e!=="full";for(let f=0;f<c;f++){let x;try{x=await _t(t,u,s)}catch(h){l=String(h?.message??h);break}if(n!==U)return;p||(S.clear(),p=!0);let g=0;for(const h of x?.data??[])S.has(h.id)||(at({id:h.id,time:h.time?.created??0,text:yt(h)}),g++);F();const b=x?.cursor?.next;if(!b||g===0)break;s=b}}finally{j=!1,z=t,et=l,Tt(S.size===0?l||"暂无用户消息":""),R&&(R=!1,it())}},it=()=>{L||(L=setTimeout(()=>{L=void 0;const t=i.untrack($);t&&G(t,"light")},300))},lt=t=>{if(!(!t||t!==i.untrack($))){if(j){R=!0;return}it()}},Ct=t=>{const e=t?.type;if(e==="message.updated"){const n=t?.data??{},l=n.info;l?.role==="user"&&l?.id&&(at({id:l.id,time:l.time?.created??0,text:yt(l)}),F()),lt(n.sessionID)}else if(e==="message.removed"){const n=t?.data??{};n.messageID&&S.delete(n.messageID)&&F(),lt(n.sessionID)}else if(e==="location.shutdown"){const n=i.untrack($);n&&z===n&&G(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,Ct(u.value)}}catch{}try{await c.return?.()}catch{}if(t)break;e++,await St(Math.min(2e3*e,15e3))}})(),i.onCleanup(()=>{t=!0,clearTimeout(n),L&&clearTimeout(L)})}),i.createEffect(()=>{const t=$();t&&i.untrack(()=>{z!==t&&(U++,j=!1,R=!1,S.clear(),m([]),C(""),W="",G(t,"full"))})});let q;const M=()=>(q?.isConnected||(q=nt()),q),At=()=>{const t=M();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&&C(s);break}};let H=!1;const ct=()=>{H||(H=!0,requestAnimationFrame(()=>{H=!1,At()}))};i.onMount(()=>{const t=()=>ct();document.addEventListener("scroll",t,!0);const e=setInterval(ct,1500);i.onCleanup(()=>{document.removeEventListener("scroll",t,!0),clearInterval(e)})});let dt=0;const w=(t,e=2600)=>{const n=++dt;ot(t),setTimeout(()=>{n===dt&&ot("")},e)};i.onMount(()=>{const t=n=>{if(n.key==="Escape"){if(i.untrack(y)){ht(),n.stopPropagation();return}i.untrack(v)&&(T(!1),n.stopPropagation())}},e=n=>{if(!i.untrack(v))return;(n.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||T(!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(`${Y} · ${Z(r)}`),e=()=>T(n=>!n);window.addEventListener(vt,t),window.addEventListener(tt,e),i.onCleanup(()=>{window.removeEventListener(vt,t),window.removeEventListener(tt,e)}),w(`会话轮次 ${Y} 已加载`)});let W="",E=0,ut=0,pt="";const V=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{}},Lt=()=>{for(const t of d())if(_(t.id))return t},ft=(t,e)=>{Zt(`${t}#${Q}${e}`),W=e},gt=()=>{const e=M()?.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}`},Mt=(t,e,n)=>new Promise(l=>{let s=!1,c,u,p;const f=Date.now(),x=gt();let g=x,b=f;const h=B=>{if(!s){s=!0;try{c?.disconnect()}catch{}clearInterval(u),clearTimeout(p),l(B??null)}},mt=B=>{if(e!==E)return h(null);const wt=B?_(t):document.getElementById(`${Q}${t}`);if(wt)return h(wt);if(!B)return;const xt=gt(),K=Date.now();if(xt!==g){g=xt,b=K;return}K-f>Gt&&g!==x&&K-b>350&&h(null)},bt=_(t);if(bt)return h(bt);c=new MutationObserver(()=>mt(!1)),c.observe(document.documentElement,{childList:!0,subtree:!0}),u=setInterval(()=>mt(!0),120),p=setTimeout(()=>h(_(t)),Math.max(400,n))}),ht=()=>{E++,P(!1),A(0),D(""),w("已取消跳转")},It=async t=>{if(y()&&N()===t.id){w(`正在跳转 · 第 ${J()} 页`,O);return}const e=++E;P(!1),A(0),D(""),rt=t.id;const n=Date.now();if(t.id===pt&&n-ut<Wt){const p=_(t.id),f=M();if(p&&f){X(f,p),V(p);return}}ut=n,pt=t.id;const l=M(),s=_(t.id);if(s&&l){X(l,s),V(s),w("已定位");return}const c=Yt(r);if(!c){w(`无法读取当前路由,无法定位 · ${Z(r)}`);return}const u=$();P(!0),D(t.id),A(0),T(!0),w("正在定位…",O);try{const p=Date.now()+O;for(let f=0;f<zt&&Date.now()<p;f++){if(e!==E||$()!==u)return;if(A(f+1),w(`跳转中 · 第 ${f+1} 页`,O),W===t.id){const b=Lt()??d()[0];b&&(ft(c,b.id),await St(Ht))}if(e!==E||$()!==u)return;ft(c,t.id);const x=Math.min(Ft,p-Date.now()),g=await Mt(t.id,e,x);if(e!==E||$()!==u)return;if(g){const b=M()??l;b&&X(b,g),V(g),w("已定位"),Kt(g);return}}e===E&&w(`未能定位 · ${Z(r)}`)}finally{e===E&&(P(!1),A(0),D(""))}};i.createEffect(()=>{y()&&N()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Pt=[a.createComponent(i.Show,{get when(){return st()},get children(){var t=Dt();return a.insert(t,st),t}}),(()=>{var t=Rt(),e=t.firstChild,n=e.firstChild,l=e.nextSibling;return t.addEventListener("mouseleave",()=>{y()||T(!1)}),t.addEventListener("mouseenter",()=>T(!0)),a.insert(e,a.createComponent(i.Show,{get when(){return y()},get fallback(){return(()=>{var s=qt();return s.firstChild,a.insert(s,(()=>{var c=a.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),s})()},get children(){var s=jt();return s.firstChild,a.insert(s,(()=>{var c=a.memo(()=>J()>0);return()=>c()?` · 第 ${J()} 页`:""})(),null),s}}),n),n.$$click=()=>{y()?ht():T(!1)},a.insert(l,a.createComponent(i.Show,{get when(){return d().length>0},get fallback(){return(()=>{var s=Bt();return a.insert(s,()=>Et()||`… ${Y}`),s})()},get children(){return a.createComponent(i.For,{get each(){return d()},children:(s,c)=>(()=>{var u=Ot(),p=u.firstChild,f=p.firstChild;f.firstChild;var x=f.nextSibling;return u.$$click=()=>{It(s)},a.insert(f,()=>c()+1,null),a.insert(x,()=>Vt(s.created)),a.insert(p,()=>s.text,null),a.effect(g=>{var b=k()===s.id?"true":"false",h=y()&&N()===s.id?"true":"false";return b!==g.e&&a.setAttribute(u,"data-active",g.e=b),h!==g.t&&a.setAttribute(u,"data-busy",g.t=h),g},{e:void 0,t:void 0}),u})()})}})),a.effect(s=>{var c=v()?"true":"false",u=y()?"true":"false",p=y()?"取消跳转 (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),p!==s.a&&a.setAttribute(n,"title",s.a=p),s},{e:void 0,t:void 0,a:void 0}),t})()];return a.createComponent(a.Portal,{get mount(){return document.body},children:Pt})}const te=I.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(I.Style,Xt),o.add(I.Slot,{at:"window.bottom",render:()=>a.createComponent(i.Show,{get when(){return o.sessions.current()},get children(){return a.createComponent(Qt,{session:()=>o.sessions.current()})}})}),o.add(I.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(tt))}}))}});a.delegateEvents(["click"]);exports.default=te;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-session-turns",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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 = "v12"
9
+ const VERSION = "v13"
10
10
  const HASH_PREFIX = "message-"
11
11
  const DIAG_EVENT = "session-turns:diag"
12
12
  const OPEN_EVENT = "session-turns:open"
@@ -18,12 +18,13 @@ const FULL_PAGE_SIZE = 200
18
18
  const LIGHT_PAGE_SIZE = 50
19
19
 
20
20
  // jump loop budgets. One round pages in roughly one host history page (~5
21
- // turns); a round ends as soon as the host appends rows, so the loop is paced
22
- // by real progress instead of a fixed sleep.
21
+ // turns); a round ends once the host's row set has stopped changing, so a
22
+ // half-finished page is never interrupted by the next hash toggle.
23
23
  const JUMP_ROUNDS = 120
24
24
  const JUMP_TOTAL_MS = 45000
25
- const JUMP_ROW_TIMEOUT_MS = 4000
25
+ const JUMP_ROW_TIMEOUT_MS = 3000
26
26
  const JUMP_SETTLE_MS = 300
27
+ const JUMP_TOGGLE_DWELL_MS = 220
27
28
  const SAME_TARGET_WINDOW_MS = 800
28
29
 
29
30
  // Filled by Rail.load()/jump() so the command palette can report diagnostics.
@@ -89,12 +90,33 @@ const diagnostics = (ctx) => {
89
90
  }
90
91
 
91
92
  const scrollToRow = (scroller, row) => {
92
- // Native scrolling reaches the real scroll ancestor without depending on
93
- // the viewport selector; then compensate for the sticky session title.
94
- row.scrollIntoView({ block: "start" })
93
+ // Align by rects instead of scrollIntoView: the host puts the row top flush
94
+ // against the sticky session title, but a 16px gradient fade sits right below
95
+ // it and would cover the top of the bubble (and our outline). Leave 24px so
96
+ // the bubble clears both the title and the fade.
95
97
  const title = scroller.querySelector("[data-session-title]")
96
- const inset = title instanceof HTMLElement ? title.offsetHeight : 0
97
- if (inset) scroller.scrollTop -= inset + 12
98
+ const scRect = scroller.getBoundingClientRect()
99
+ const titleBottom =
100
+ title instanceof HTMLElement ? title.getBoundingClientRect().bottom : scRect.top
101
+ const rowTop = row.getBoundingClientRect().top
102
+ const delta = rowTop - (titleBottom + 24)
103
+ if (Math.abs(delta) > 2) scroller.scrollTop += delta
104
+ }
105
+
106
+ // The host's own hash seek may finish after ours and re-align the row flush
107
+ // under the sticky title (its 16px fade then covers the bubble's top edge).
108
+ // Repair only that case: if the user already scrolled elsewhere, leave it be.
109
+ const repairScroll = (row) => {
110
+ setTimeout(() => {
111
+ if (!row.isConnected) return
112
+ const sc = findScroller()
113
+ if (!sc) return
114
+ const title = sc.querySelector("[data-session-title]")
115
+ const titleBottom =
116
+ title instanceof HTMLElement ? title.getBoundingClientRect().bottom : sc.getBoundingClientRect().top
117
+ const rowTop = row.getBoundingClientRect().top
118
+ if (rowTop < titleBottom + 4) sc.scrollTop += rowTop - (titleBottom + 24)
119
+ }, 300)
98
120
  }
99
121
 
100
122
  // The desktop app (2.2.6) routes through a memory history, so window.location is
@@ -733,6 +755,8 @@ function Rail(props) {
733
755
  let timer
734
756
  const started = Date.now()
735
757
  const baseline = rowSignature()
758
+ let lastSig = baseline
759
+ let stableSince = started
736
760
  const finish = (row) => {
737
761
  if (done) return
738
762
  done = true
@@ -745,12 +769,24 @@ function Rail(props) {
745
769
  }
746
770
  const check = (full) => {
747
771
  if (token !== jumpToken) return finish(null)
748
- const row = findRow(messageID)
772
+ // Observer callbacks use the cheap id lookup; only the poll pays for the
773
+ // data-attribute fallback (a full scan) and the row fingerprint.
774
+ const row = full ? findRow(messageID) : document.getElementById(`${HASH_PREFIX}${messageID}`)
749
775
  if (row) return finish(row)
750
- // Let the click settle before trusting a DOM change as "a page arrived".
751
- // Only the poll evaluates the row fingerprint: streaming output fires
752
- // the observer constantly and a full query on every mutation is wasted.
753
- if (full && Date.now() - started > JUMP_SETTLE_MS && rowSignature() !== baseline) finish(null)
776
+ if (!full) return
777
+ const sig = rowSignature()
778
+ const now = Date.now()
779
+ if (sig !== lastSig) {
780
+ lastSig = sig
781
+ stableSince = now
782
+ return
783
+ }
784
+ // Only treat the page as done once the host has stopped appending rows
785
+ // for a moment: toggling the hash mid-load cancels its paging and the
786
+ // jump stalls until the user clicks again.
787
+ if (now - started > JUMP_SETTLE_MS && lastSig !== baseline && now - stableSince > 350) {
788
+ finish(null)
789
+ }
754
790
  }
755
791
  const existing = findRow(messageID)
756
792
  if (existing) return finish(existing)
@@ -769,6 +805,15 @@ function Rail(props) {
769
805
  }
770
806
 
771
807
  const jump = async (turn) => {
808
+ // Clicking the same turn again while its jump is running must NOT restart
809
+ // the loop: every restart cancels the host's in-flight page load, which is
810
+ // what makes a deep jump advance only one page per click. Re-affirm the
811
+ // progress instead and let the running loop finish.
812
+ if (jumping() && busyID() === turn.id) {
813
+ flash(`正在跳转 · 第 ${jumpPage()} 页`, JUMP_TOTAL_MS)
814
+ return
815
+ }
816
+
772
817
  const token = ++jumpToken
773
818
  // A new click always owns the progress UI. This also clears a previous
774
819
  // jump's state when the new target takes the mounted fast path, where the
@@ -836,20 +881,24 @@ function Rail(props) {
836
881
  const other = oldestLoadedTurn() ?? turns()[0]
837
882
  if (other) {
838
883
  navTo(path, other.id)
839
- await sleep(120)
884
+ // Let the host commit the intermediate hash before seeking back;
885
+ // too fast and both clicks collapse into the same target hash, so
886
+ // the cursor machine never runs another page.
887
+ await sleep(JUMP_TOGGLE_DWELL_MS)
840
888
  }
841
889
  }
842
- if (token !== jumpToken) return
890
+ if (token !== jumpToken || sessionID() !== sessionAtJump) return
843
891
  navTo(path, turn.id)
844
892
 
845
893
  const remaining = Math.min(JUMP_ROW_TIMEOUT_MS, deadline - Date.now())
846
894
  const landed = await waitForRow(turn.id, token, remaining)
847
- if (token !== jumpToken) return
895
+ if (token !== jumpToken || sessionID() !== sessionAtJump) return
848
896
  if (landed) {
849
897
  const live = scroller() ?? sc
850
898
  if (live) scrollToRow(live, landed)
851
899
  pulse(landed)
852
900
  flash("已定位")
901
+ repairScroll(landed)
853
902
  return
854
903
  }
855
904
  }