opencode-session-turns 0.1.12 → 0.1.13

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 l=require("solid-js/web"),B=require("@opencode/plugin/gui"),a=require("solid-js");var ee=l.template("<div class=sr-status>"),ne=l.template('<span class="sr-title sr-progress">'),re=l.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-warm title=预热历史:后台把会话历史加载完,之后跳转瞬时>预热</button><button type=button class=sr-close>×</button></div><div class=sr-list>"),se=l.template("<span class=sr-title>会话轮次"),oe=l.template("<div class=sr-note>"),ie=l.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 at="v16",ft="message-",Bt="session-turns:diag",gt="session-turns:open",lt="session-turns:warm",ae=40,le=200,ce=50,de=30,q=45e3,ue=12e3,pe=5e3,fe=9e3,ge=220,he=800;let Ht=0,ht="",mt="",bt="";const Jt=o=>{const r=String(o?.text??"").split(`
2
- `).map(g=>g.trim()).find(g=>g.length>0);if(r)return r;const u=o?.files?.length??0;return u>0?`[附件 ×${u}]`:"(空消息)"},Ot=o=>String(o).padStart(2,"0"),me=o=>{const r=new Date(o??0);return`${Ot(r.getHours())}:${Ot(r.getMinutes())}`},ct=o=>new Promise(r=>setTimeout(r,o)),wt=()=>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 u=getComputedStyle(r);if(/(auto|scroll)/.test(u.overflowY)&&r.scrollHeight>r.clientHeight+1)return r;r=r.parentElement}})(),k=o=>document.getElementById(`${ft}${o}`)??[...document.querySelectorAll("[data-message-id]")].find(r=>r.getAttribute("data-message-id")===o),dt=o=>{const r=wt(),u=[...document.querySelectorAll("[data-message-id]")],g=u[0]?.getAttribute("data-message-id")??"",E=bt?` · 目标 ${k(bt)?"在":"不在"} DOM`:"",_=g?` · 行ID ${g.slice(0,20)}`:"";let I="(空)";try{I=o.router.path()||"(空)"}catch{}return`轮次 ${Ht} · 容器 ${r?"OK":"缺失"} · DOM ${u.length} 行${E}${_} · 路由 ${I} · hash ${window.location.hash||"(空)"}${mt?` · ${mt}`:""}${ht?` · ${ht}`:""}`},be=o=>{const r=o.getBoundingClientRect().top;let u=r;for(const g of o.querySelectorAll(".sticky, [data-session-title]")){if(!(g instanceof HTMLElement)||getComputedStyle(g).position!=="sticky"&&!g.hasAttribute("data-session-title"))continue;const _=g.getBoundingClientRect();_.height!==0&&(_.top>r+4||(u=Math.max(u,_.bottom)))}return u},ut=o=>{try{const r=wt();if(!r||!o.isConnected)return;const u=o.querySelector('[data-slot="user-message-text"]')??o,g=be(r),E=u.getBoundingClientRect().top-(g+18);Math.abs(E)>2&&(r.scrollTop+=E),mt=`inset ${Math.round(g)} · delta ${Math.round(E)}`}catch{}},pt=o=>{ut(o),setTimeout(()=>ut(o),450),setTimeout(()=>ut(o),1e3)},Nt=o=>{let r="";try{r=o.router.path()||""}catch{}return r.startsWith("/")?r:""},we=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()}},ve=`
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("solid-js/web"),q=require("@opencode/plugin/gui"),l=require("solid-js");var Xt=a.template("<div class=sr-status>"),Qt=a.template('<span class="sr-title sr-progress">跳转中'),te=a.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),ee=a.template("<span class=sr-title>会话轮次"),ne=a.template("<div class=sr-note>"),re=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 Lt="v16",ct="message-",Dt="session-turns:diag",dt="session-turns:open",se=40,oe=200,ie=50,ae=30,Pt=45e3,le=2e4,ce=8e3,de=12e3,ue=220,pe=800;let Bt=0,ut="",pt="",ft="";const jt=o=>{const s=String(o?.text??"").split(`
2
+ `).map(p=>p.trim()).find(p=>p.length>0);if(s)return s;const d=o?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},Rt=o=>String(o).padStart(2,"0"),fe=o=>{const s=new Date(o??0);return`${Rt(s.getHours())}:${Rt(s.getMinutes())}`},ot=o=>new Promise(s=>setTimeout(s,o)),gt=()=>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}})(),x=o=>{const s=document.getElementById(`${ct}${o}`);if(s)return s;const d=`[data-message-id="${o.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},it=o=>{const s=gt(),d=[...document.querySelectorAll("[data-message-id]")],p=d[0]?.getAttribute("data-message-id")??"",S=ft?` · 目标 ${x(ft)?"在":"不在"} DOM`:"",$=p?` · 行ID ${p.slice(0,20)}`:"";let M="(空)";try{M=o.router.path()||"(空)"}catch{}return`轮次 ${Bt} · 容器 ${s?"OK":"缺失"} · DOM ${d.length} 行${S}${$} · 路由 ${M} · hash ${window.location.hash||"(空)"}${pt?` · ${pt}`:""}${ut?` · ${ut}`:""}`},ge=o=>{const s=o.getBoundingClientRect().top;let d=s;for(const p of o.querySelectorAll(".sticky, [data-session-title]")){if(!(p instanceof HTMLElement)||getComputedStyle(p).position!=="sticky"&&!p.hasAttribute("data-session-title"))continue;const $=p.getBoundingClientRect();$.height!==0&&($.top>s+4||(d=Math.max(d,$.bottom)))}return d},at=o=>{try{const s=gt();if(!s||!o.isConnected)return;const d=o.querySelector('[data-slot="user-message-text"]')??o,p=ge(s),S=d.getBoundingClientRect().top-(p+18);Math.abs(S)>2&&(s.scrollTop+=S),pt=`inset ${Math.round(p)} · delta ${Math.round(S)}`}catch{}},lt=o=>{at(o),setTimeout(()=>at(o),450),setTimeout(()=>at(o),1e3)},qt=o=>{let s="";try{s=o.router.path()||""}catch{}return s.startsWith("/")?s:""},he=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()}},me=`
3
3
  .sr-rail {
4
4
  position: fixed;
5
5
  top: 50%;
@@ -60,20 +60,6 @@
60
60
  flex-shrink: 0;
61
61
  }
62
62
  .sr-close:hover { opacity: 1; background: var(--surface-base-hover, rgba(127,127,127,.14)); }
63
- .sr-warm {
64
- border: 1px solid rgba(127,127,127,.28);
65
- background: transparent;
66
- color: inherit;
67
- font: inherit;
68
- font-size: 11px;
69
- line-height: 1;
70
- padding: 3px 7px;
71
- border-radius: 5px;
72
- cursor: pointer;
73
- opacity: .6;
74
- flex-shrink: 0;
75
- }
76
- .sr-warm:hover { opacity: 1; background: var(--surface-base-hover, rgba(127,127,127,.14)); }
77
63
  /* the scrollable list; the header stays fixed above it */
78
64
  .sr-list {
79
65
  flex: 1;
@@ -228,4 +214,4 @@
228
214
  white-space: pre-wrap;
229
215
  max-width: min(720px, 90vw);
230
216
  }
231
- `;function xe(o){const r=B.usePlugin(),[u,g]=a.createSignal([]),[E,_]=a.createSignal(""),[I,L]=a.createSignal(!1),[zt,Ft]=a.createSignal(""),[vt,xt]=a.createSignal(""),[x,J]=a.createSignal(!1),[V,P]=a.createSignal(0),[K,D]=a.createSignal(0),[ke,O]=a.createSignal(0),[Y,N]=a.createSignal(""),A=new Map;let Z=0,H=!1,z=!1,X="",j;const y=()=>o.session()?.id,Q=()=>{const t=[...A.values()].sort((n,i)=>n.created-i.created),e=a.untrack(u);e.length===t.length&&e.every((n,i)=>n===t[i])||(g(t),Ht=t.length)},yt=t=>{const e=A.get(t.id);e&&e.created===t.time&&e.text===t.text||A.set(t.id,{id:t.id,created:t.time,text:t.text})},Wt=async(t,e,n)=>{const i=n?{sessionID:t,type:"user",limit:e,cursor:n}:{sessionID:t,type:"user",limit:e,order:"desc"};return await r.client.message.list(i)},tt=async(t,e)=>{const n=++Z;H=!0;let i="";try{let c;const s=e==="full"?ae:1,p=e==="full"?le:ce;let f=e!=="full";for(let h=0;h<s;h++){let m;try{m=await Wt(t,p,c)}catch(b){i=String(b?.message??b);break}if(n!==Z)return;f||(A.clear(),f=!0);let v=0;for(const b of m?.data??[])A.has(b.id)||(yt({id:b.id,time:b.time?.created??0,text:Jt(b)}),v++);Q();const d=m?.cursor?.next;if(!d||v===0)break;c=d}}finally{H=!1,X=t,ht=i,Ft(A.size===0?i||"暂无用户消息":""),z&&(z=!1,kt())}},kt=()=>{j||(j=setTimeout(()=>{j=void 0;const t=a.untrack(y);t&&tt(t,"light")},300))},$t=t=>{if(!(!t||t!==a.untrack(y))){if(H){z=!0;return}kt()}},Ut=t=>{const e=t?.type;if(e==="message.updated"){const n=t?.data??{},i=n.info;i?.role==="user"&&i?.id&&(yt({id:i.id,time:i.time?.created??0,text:Jt(i)}),Q()),$t(n.sessionID)}else if(e==="message.removed"){const n=t?.data??{};n.messageID&&A.delete(n.messageID)&&Q(),$t(n.sessionID)}else if(e==="location.shutdown"){const n=a.untrack(y);n&&X===n&&tt(n,"full")}};a.onMount(()=>{let t=!1,e=0,n;(async()=>{for(;!t;){const s=r.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!t;){const p=await s.next();if(p.done)break;e=0,Ut(p.value)}}catch{}try{await s.return?.()}catch{}if(t)break;e++,await ct(Math.min(2e3*e,15e3))}})(),a.onCleanup(()=>{t=!0,clearTimeout(n),j&&clearTimeout(j)})}),a.createEffect(()=>{const t=y();t&&a.untrack(()=>{X!==t&&(Z++,H=!1,z=!1,A.clear(),g([]),_(""),nt="",tt(t,"full"))})});let F;const C=()=>(F?.isConnected||(F=wt()),F),Gt=()=>{const t=C();if(!t)return;const e=t.querySelectorAll("[data-message-id]");if(e.length===0)return;const n=t.getBoundingClientRect().top+40;for(const i of e)if(i.getBoundingClientRect().bottom>=n){const c=i.getAttribute("data-message-id");c&&_(c);break}};let et=!1;const St=()=>{et||(et=!0,requestAnimationFrame(()=>{et=!1,Gt()}))};a.onMount(()=>{const t=()=>St();document.addEventListener("scroll",t,!0);const e=setInterval(St,1500);a.onCleanup(()=>{document.removeEventListener("scroll",t,!0),clearInterval(e)})});let Et=0;const w=(t,e=2600)=>{const n=++Et;xt(t),setTimeout(()=>{n===Et&&xt("")},e)};a.onMount(()=>{const t=n=>{if(n.key==="Escape"){if(a.untrack(x)){Ct(),n.stopPropagation();return}a.untrack(I)&&(L(!1),n.stopPropagation())}},e=n=>{if(!a.untrack(I))return;(n.composedPath?.()??[]).some(s=>s instanceof Element&&s.closest?.(".sr-rail"))||L(!1)};window.addEventListener("keydown",t,!0),document.addEventListener("pointerdown",e,!0),a.onCleanup(()=>{window.removeEventListener("keydown",t,!0),document.removeEventListener("pointerdown",e,!0)})}),a.onMount(()=>{const t=()=>w(`${at} · ${dt(r)}`),e=()=>{L(i=>!i),It()},n=()=>{Pt()};window.addEventListener(Bt,t),window.addEventListener(gt,e),window.addEventListener(lt,n),a.onCleanup(()=>{window.removeEventListener(Bt,t),window.removeEventListener(gt,e),window.removeEventListener(lt,n)}),w(`会话轮次 ${at} 已加载`)});let nt="",T=0,_t=0,At="";const rt=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{}},Vt=()=>{for(const t of u())if(k(t.id))return t},st=(t,e)=>{we(`${t}#${ft}${e}`),nt=e},W=()=>{const e=C()?.querySelectorAll("[data-message-id]")??[];if(e.length===0)return"0";const n=e[0].getAttribute("data-message-id")??"",i=e[e.length-1].getAttribute("data-message-id")??"";return`${e.length}:${n}:${i}`},Kt=(t,e,n,i)=>new Promise(c=>{let s=!1,p,f,h;const m=Date.now();let d=W(),b=m,M=0;const $=G=>{if(!s){s=!0;try{p?.disconnect()}catch{}clearInterval(f),clearTimeout(h),c(G??null)}},S=G=>{if(e!==T)return $(null);const jt=G?k(t):document.getElementById(`${ft}${t}`);if(jt)return $(jt);if(!G)return;const Rt=W(),qt=Date.now();if(Rt!==d){d=Rt,b=qt,M++,i?.(M);return}const te=M>0?pe:fe;qt-b>te&&$(null)},Dt=k(t);if(Dt)return $(Dt);p=new MutationObserver(()=>S(!1)),p.observe(document.documentElement,{childList:!0,subtree:!0}),f=setInterval(()=>S(!0),120),h=setTimeout(()=>$(k(t)),Math.max(400,n))}),Ct=()=>{T++,J(!1),P(0),D(0),O(0),N(""),w("已取消跳转")},Yt=async t=>{if(U()&&Xt(),x()&&Y()===t.id){w(`正在跳转 · 已加载 ${V()} 页 · ${K()}s`,q);return}const e=++T;J(!1),P(0),D(0),O(0),N(""),bt=t.id;const n=Date.now();if(t.id===At&&n-_t<he){const v=k(t.id),d=C();if(v&&d){pt(v),rt(v);return}}_t=n,At=t.id;const i=C(),c=k(t.id);if(c&&i){pt(c),rt(c),w("已定位");return}const s=Nt(r);if(!s){w(`无法读取当前路由,无法定位 · ${dt(r)}`);return}const p=y();J(!0),N(t.id),P(0),D(0),L(!0),w("正在定位…",q);const f=Date.now(),h=setInterval(()=>{D(Math.round((Date.now()-f)/1e3))},500);let m=0;try{const v=Date.now()+q;for(let d=0;d<de&&Date.now()<v;d++){if(e!==T||y()!==p)return;if(O(d+1),nt===t.id){const S=Vt()??u()[0];S&&(st(s,S.id),await ct(ge))}if(e!==T||y()!==p)return;st(s,t.id),w(`跳转中 · 已加载 ${m} 页`,q);let b=0;const M=Math.min(ue,v-Date.now()),$=await Kt(t.id,e,M,S=>{b=S,P(m+S),w(`跳转中 · 已加载 ${m+S} 页`,q)});if(m+=b,e!==T||y()!==p)return;if($){pt($),rt($),w("已定位");return}}e===T&&w(`未能定位 · ${dt(r)}`)}finally{clearInterval(h),e===T&&(J(!1),P(0),D(0),O(0),N(""))}},[U,ot]=a.createSignal(!1),[Tt,Mt]=a.createSignal(0);let R=0,it="";const Zt=()=>{const t=C();if(!t)return;const e=t.getBoundingClientRect();for(const n of t.querySelectorAll("[data-message-id]")){const i=n.getBoundingClientRect();if(i.bottom<=e.top+1)continue;const c=n.getAttribute("data-message-id");if(c)return{id:c,offset:i.top-e.top}}},Lt=t=>{if(!t)return;const e=C();if(!e)return;const n=k(t.id);if(!n)return;const i=e.getBoundingClientRect(),c=n.getBoundingClientRect().top-i.top-t.offset;Math.abs(c)>2&&(e.scrollTop+=c)},Xt=()=>{R++,ot(!1)},It=()=>{const t=y();!t||U()||it!==t&&(u().length<15||Pt())},Pt=async()=>{if(U())return;const t=Nt(r),e=u()[0],n=y();if(!t||!e||!n)return;if(it===n&&k(e.id)){w("历史已就绪");return}const i=++R,c=Zt(),s=C(),p=s?s.scrollTop+s.clientHeight>=s.scrollHeight-32:!0;ot(!0),Mt(0),w("正在预热历史…",12e4);try{st(t,e.id);const f=Date.now()+12e4;let h=0,m=W();const v=setInterval(()=>Lt(c),150);try{for(;Date.now()<f&&i===R&&!k(e.id);){await ct(250);const d=W();d!==m&&(m=d,h++,Mt(h))}}finally{clearInterval(v)}if(i!==R)return;if(Lt(c),p){const d=C();d&&(d.scrollTop=d.scrollHeight)}it=n,w(`历史已预热(${h} 页),现在跳转是瞬时的`)}finally{i===R&&ot(!1)}};a.createEffect(()=>{x()&&Y()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Qt=[l.createComponent(a.Show,{get when(){return vt()},get children(){var t=ee();return l.insert(t,vt),t}}),(()=>{var t=re(),e=t.firstChild,n=e.firstChild,i=n.nextSibling,c=e.nextSibling;return t.addEventListener("mouseleave",()=>{x()||L(!1)}),t.addEventListener("mouseenter",()=>{L(!0),It()}),l.insert(e,l.createComponent(a.Show,{get when(){return x()||U()},get fallback(){return(()=>{var s=se();return s.firstChild,l.insert(s,(()=>{var p=l.memo(()=>u().length>0);return()=>p()?` · ${u().length}`:""})(),null),s})()},get children(){var s=ne();return l.insert(s,(()=>{var p=l.memo(()=>!!x());return()=>p()?`跳转中${V()>0?` · 已加载 ${V()} 页`:""}${K()>0?` · ${K()}s`:""}`:`预热中${Tt()>0?` · ${Tt()} 页`:""}`})()),s}}),n),n.$$click=()=>window.dispatchEvent(new Event(lt)),i.$$click=()=>{x()?Ct():L(!1)},l.insert(c,l.createComponent(a.Show,{get when(){return u().length>0},get fallback(){return(()=>{var s=oe();return l.insert(s,()=>zt()||`… ${at}`),s})()},get children(){return l.createComponent(a.For,{get each(){return u()},children:(s,p)=>(()=>{var f=ie(),h=f.firstChild,m=h.firstChild;m.firstChild;var v=m.nextSibling;return f.$$click=()=>{Yt(s)},l.insert(m,()=>p()+1,null),l.insert(v,()=>me(s.created)),l.insert(h,()=>s.text,null),l.effect(d=>{var b=E()===s.id?"true":"false",M=x()&&Y()===s.id?"true":"false";return b!==d.e&&l.setAttribute(f,"data-active",d.e=b),M!==d.t&&l.setAttribute(f,"data-busy",d.t=M),d},{e:void 0,t:void 0}),f})()})}})),l.effect(s=>{var p=I()?"true":"false",f=x()?"true":"false",h=x()?"取消跳转 (Esc)":"关闭 (Esc)";return p!==s.e&&l.setAttribute(t,"data-open",s.e=p),f!==s.t&&l.setAttribute(t,"data-jumping",s.t=f),h!==s.a&&l.setAttribute(i,"title",s.a=h),s},{e:void 0,t:void 0,a:void 0}),t})()];return l.createComponent(l.Portal,{get mount(){return document.body},children:Qt})}const ye=B.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(B.Style,ve),o.add(B.Slot,{at:"window.bottom",render:()=>l.createComponent(a.Show,{get when(){return o.sessions.current()},get children(){return l.createComponent(xe,{session:()=>o.sessions.current()})}})}),o.add(B.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(gt))}}))}});l.delegateEvents(["click"]);exports.default=ye;
217
+ `;function be(o){const s=q.usePlugin(),[d,p]=l.createSignal([]),[S,$]=l.createSignal(""),[M,T]=l.createSignal(!1),[Jt,Ot]=l.createSignal(""),[ht,mt]=l.createSignal(""),[y,B]=l.createSignal(!1),[G,L]=l.createSignal(0),[W,D]=l.createSignal(0),[ve,J]=l.createSignal(0),[V,O]=l.createSignal(""),E=new Map;let K=0,H=!1,N=!1,Y="",P;const v=()=>o.session()?.id,Z=()=>{const t=[...E.values()].sort((n,i)=>n.created-i.created),e=l.untrack(d);e.length===t.length&&e.every((n,i)=>n===t[i])||(p(t),Bt=t.length)},bt=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})},Ht=async(t,e,n)=>{const i=n?{sessionID:t,type:"user",limit:e,cursor:n}:{sessionID:t,type:"user",limit:e,order:"desc"};return await s.client.message.list(i)},X=async(t,e)=>{const n=++K;H=!0;let i="";try{let r;const c=e==="full"?se:1,u=e==="full"?oe:ie;let h=e!=="full";for(let w=0;w<c;w++){let m;try{m=await Ht(t,u,r)}catch(g){i=String(g?.message??g);break}if(n!==K)return;h||(E.clear(),h=!0);let f=0;for(const g of m?.data??[])E.has(g.id)||(bt({id:g.id,time:g.time?.created??0,text:jt(g)}),f++);Z();const b=m?.cursor?.next;if(!b||f===0)break;r=b}}finally{H=!1,Y=t,ut=i,Ot(E.size===0?i||"暂无用户消息":""),N&&(N=!1,wt())}},wt=()=>{P||(P=setTimeout(()=>{P=void 0;const t=l.untrack(v);t&&X(t,"light")},300))},vt=t=>{if(!(!t||t!==l.untrack(v))){if(H){N=!0;return}wt()}},Nt=t=>{const e=t?.type;if(e==="message.updated"){const n=t?.data??{},i=n.info;i?.role==="user"&&i?.id&&(bt({id:i.id,time:i.time?.created??0,text:jt(i)}),Z()),vt(n.sessionID)}else if(e==="message.removed"){const n=t?.data??{};n.messageID&&E.delete(n.messageID)&&Z(),vt(n.sessionID)}else if(e==="location.shutdown"){const n=l.untrack(v);n&&Y===n&&X(n,"full")}};l.onMount(()=>{let t=!1,e=0,n;(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,Nt(u.value)}}catch{}try{await c.return?.()}catch{}if(t)break;e++,await ot(Math.min(2e3*e,15e3))}})(),l.onCleanup(()=>{t=!0,clearTimeout(n),P&&clearTimeout(P)})}),l.createEffect(()=>{const t=v();t&&l.untrack(()=>{Y!==t&&(K++,H=!1,N=!1,E.clear(),p([]),$(""),tt="",X(t,"full"))})});let F;const _=()=>(F?.isConnected||(F=gt()),F),Ft=()=>{const t=_();if(!t)return;const e=t.querySelectorAll("[data-message-id]");if(e.length===0)return;const n=t.getBoundingClientRect().top+40;for(const i of e)if(i.getBoundingClientRect().bottom>=n){const r=i.getAttribute("data-message-id");r&&$(r);break}};let Q=!1;const xt=()=>{Q||(Q=!0,requestAnimationFrame(()=>{Q=!1,Ft()}))};l.onMount(()=>{const t=()=>xt();document.addEventListener("scroll",t,!0);const e=setInterval(xt,1500);l.onCleanup(()=>{document.removeEventListener("scroll",t,!0),clearInterval(e)})});let yt=0;const I=(t,e=2600)=>{const n=++yt;mt(t),setTimeout(()=>{n===yt&&mt("")},e)};l.onMount(()=>{const t=n=>{if(n.key==="Escape"){if(l.untrack(y)){Et(),n.stopPropagation();return}l.untrack(M)&&(T(!1),n.stopPropagation())}},e=n=>{if(!l.untrack(M))return;(n.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||T(!1)};window.addEventListener("keydown",t,!0),document.addEventListener("pointerdown",e,!0),l.onCleanup(()=>{window.removeEventListener("keydown",t,!0),document.removeEventListener("pointerdown",e,!0)})}),l.onMount(()=>{const t=()=>I(`${Lt} · ${it(s)}`),e=()=>{T(n=>!n),Ct()};window.addEventListener(Dt,t),window.addEventListener(dt,e),l.onCleanup(()=>{window.removeEventListener(Dt,t),window.removeEventListener(dt,e)})});let tt="",C=0,kt=0,St="";const et=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{}},Ut=()=>{for(const t of d())if(x(t.id))return t},nt=(t,e)=>{he(`${t}#${ct}${e}`),tt=e},$t=()=>{const e=_()?.querySelectorAll("[data-message-id]")??[];if(e.length===0)return"0";const n=e[0].getAttribute("data-message-id")??"",i=e[e.length-1].getAttribute("data-message-id")??"";return`${e.length}:${n}:${i}`},zt=(t,e,n,i)=>new Promise(r=>{let c=!1,u,h,w;const m=Date.now();let b=$t(),g=m,R=0;const k=z=>{if(!c){c=!0;try{u?.disconnect()}catch{}clearInterval(h),clearTimeout(w),r(z??null)}},A=z=>{if(e!==C)return k(null);const Tt=z?x(t):document.getElementById(`${ct}${t}`);if(Tt)return k(Tt);if(!z)return;const Mt=$t(),It=Date.now();if(Mt!==b){b=Mt,g=It,R++,i?.(R);return}const Zt=R>0?ce:de;It-g>Zt&&k(null)},At=x(t);if(At)return k(At);u=new MutationObserver(()=>A(!1)),u.observe(document.documentElement,{childList:!0,subtree:!0}),h=setInterval(()=>A(!0),120),w=setTimeout(()=>k(x(t)),Math.max(400,n))}),Et=()=>{C++,B(!1),L(0),D(0),J(0),O(""),I("已取消跳转")},Gt=async t=>{if(U()&&Vt(),y()&&V()===t.id){I(`正在跳转 · 已加载 ${G()} 页 · ${W()}s`,Pt);return}const e=++C;B(!1),L(0),D(0),J(0),O(""),ft=t.id;const n=Date.now();if(t.id===St&&n-kt<pe){const f=x(t.id),b=_();if(f&&b){lt(f),et(f);return}}kt=n,St=t.id;const i=_(),r=x(t.id);if(r&&i){lt(r),et(r);return}const c=qt(s);if(!c){I(`无法读取当前路由,无法定位 · ${it(s)}`);return}const u=v();B(!0),O(t.id),L(0),D(0),T(!0),I(U()?"正在预热历史,请稍候…":"正在加载历史…",5e3);const h=Date.now(),w=setInterval(()=>{D(Math.round((Date.now()-h)/1e3))},500);let m=0;try{const f=Date.now()+Pt;for(let b=0;b<ae&&Date.now()<f;b++){if(e!==C||v()!==u)return;if(J(b+1),tt===t.id){const A=Ut()??d()[0];A&&(nt(c,A.id),await ot(ue))}if(e!==C||v()!==u)return;nt(c,t.id);let g=0;const R=Math.min(le,f-Date.now()),k=await zt(t.id,e,R,A=>{g=A,L(m+A)});if(m+=g,e!==C||v()!==u)return;if(k){lt(k),et(k);return}}e===C&&I(`未能定位 · ${it(s)}`)}finally{clearInterval(w),e===C&&(B(!1),L(0),D(0),J(0),O(""))}},[U,rt]=l.createSignal(!1);let j=0,st="";const Wt=()=>{const t=_();if(!t)return;const e=t.getBoundingClientRect();for(const n of t.querySelectorAll("[data-message-id]")){const i=n.getBoundingClientRect();if(i.bottom<=e.top+1)continue;const r=n.getAttribute("data-message-id");if(r)return{el:n,id:r,offset:i.top-e.top}}},_t=t=>{if(!t)return;const e=_();if(!e)return;const n=t.el?.isConnected?t.el:x(t.id);if(!n)return;const i=e.getBoundingClientRect(),r=n.getBoundingClientRect().top-i.top-t.offset;Math.abs(r)>2&&(e.scrollTop+=r)},Vt=()=>{j++,rt(!1)},Ct=()=>{const t=v();!t||U()||st!==t&&(d().length<15||Kt())},Kt=async()=>{if(U())return;const t=qt(s),e=d()[0],n=v();if(!t||!e||!n||st===n&&x(e.id))return;const i=++j,r=Wt(),c=_(),u=c?c.scrollTop+c.clientHeight>=c.scrollHeight-32:!0;rt(!0);try{nt(t,e.id);const h=Date.now()+12e4,w=setInterval(()=>_t(r),150);try{for(;Date.now()<h&&i===j&&!x(e.id);)await ot(250)}finally{clearInterval(w)}if(i!==j)return;if(_t(r),u){const m=_();m&&(m.scrollTop=m.scrollHeight)}st=n}finally{i===j&&rt(!1)}};l.createEffect(()=>{y()&&V()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Yt=[a.createComponent(l.Show,{get when(){return ht()},get children(){var t=Xt();return a.insert(t,ht),t}}),(()=>{var t=te(),e=t.firstChild,n=e.firstChild,i=e.nextSibling;return t.addEventListener("mouseleave",()=>{y()||T(!1)}),t.addEventListener("mouseenter",()=>{T(!0),Ct()}),a.insert(e,a.createComponent(l.Show,{get when(){return y()},get fallback(){return(()=>{var r=ee();return r.firstChild,a.insert(r,(()=>{var c=a.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),r})()},get children(){var r=Qt();return r.firstChild,a.insert(r,(()=>{var c=a.memo(()=>G()>0);return()=>c()?` · 已加载 ${G()} 页`:""})(),null),a.insert(r,(()=>{var c=a.memo(()=>W()>0);return()=>c()?` · ${W()}s`:""})(),null),r}}),n),n.$$click=()=>{y()?Et():T(!1)},a.insert(i,a.createComponent(l.Show,{get when(){return d().length>0},get fallback(){return(()=>{var r=ne();return a.insert(r,()=>Jt()||`… ${Lt}`),r})()},get children(){return a.createComponent(l.For,{get each(){return d()},children:(r,c)=>(()=>{var u=re(),h=u.firstChild,w=h.firstChild;w.firstChild;var m=w.nextSibling;return u.$$click=()=>{Gt(r)},a.insert(w,()=>c()+1,null),a.insert(m,()=>fe(r.created)),a.insert(h,()=>r.text,null),a.effect(f=>{var b=S()===r.id?"true":"false",g=y()&&V()===r.id?"true":"false";return b!==f.e&&a.setAttribute(u,"data-active",f.e=b),g!==f.t&&a.setAttribute(u,"data-busy",f.t=g),f},{e:void 0,t:void 0}),u})()})}})),a.effect(r=>{var c=M()?"true":"false",u=y()?"true":"false",h=y()?"取消跳转 (Esc)":"关闭 (Esc)";return c!==r.e&&a.setAttribute(t,"data-open",r.e=c),u!==r.t&&a.setAttribute(t,"data-jumping",r.t=u),h!==r.a&&a.setAttribute(n,"title",r.a=h),r},{e:void 0,t:void 0,a:void 0}),t})()];return a.createComponent(a.Portal,{get mount(){return document.body},children:Yt})}const we=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(o){o.add(q.Style,me),o.add(q.Slot,{at:"window.bottom",render:()=>a.createComponent(l.Show,{get when(){return o.sessions.current()},get children(){return a.createComponent(be,{session:()=>o.sessions.current()})}})}),o.add(q.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(dt))}}))}});a.delegateEvents(["click"]);exports.default=we;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-session-turns",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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
@@ -10,7 +10,6 @@ const VERSION = "v16"
10
10
  const HASH_PREFIX = "message-"
11
11
  const DIAG_EVENT = "session-turns:diag"
12
12
  const OPEN_EVENT = "session-turns:open"
13
- const WARM_EVENT = "session-turns:warm"
14
13
 
15
14
  // paging: a full load walks every page; a light refresh pulls just the newest
16
15
  // page (new messages always land at the head of a desc listing).
@@ -27,9 +26,9 @@ const LIGHT_PAGE_SIZE = 50
27
26
  // assumes the chain died and starts a new one.
28
27
  const JUMP_ROUNDS = 30
29
28
  const JUMP_TOTAL_MS = 45000
30
- const JUMP_CHAIN_MS = 12000
31
- const JUMP_IDLE_MS = 5000
32
- const JUMP_FIRST_IDLE_MS = 9000
29
+ const JUMP_CHAIN_MS = 20000
30
+ const JUMP_IDLE_MS = 8000
31
+ const JUMP_FIRST_IDLE_MS = 12000
33
32
  const JUMP_TOGGLE_DWELL_MS = 220
34
33
  const SAME_TARGET_WINDOW_MS = 800
35
34
 
@@ -77,11 +76,21 @@ const findScroller = () =>
77
76
 
78
77
  // The app sets id="message-<id>" on every timeline row. Prefer that over the
79
78
  // data attribute: Solid Router's own scrollToHash() uses getElementById too.
80
- const findRow = (messageID) =>
81
- document.getElementById(`${HASH_PREFIX}${messageID}`) ??
82
- [...document.querySelectorAll("[data-message-id]")].find(
83
- (node) => node.getAttribute("data-message-id") === messageID,
84
- )
79
+ //
80
+ // IMPORTANT: `data-message-id` is set on assistant rows as well (they carry
81
+ // the id of the user message they belong to), so a naive lookup can return an
82
+ // assistant row and we would scroll to the reply instead of the question.
83
+ // Only the user message row counts as "found".
84
+ const findRow = (messageID) => {
85
+ const anchored = document.getElementById(`${HASH_PREFIX}${messageID}`)
86
+ if (anchored) return anchored
87
+ const selector = `[data-message-id="${messageID.replace(/"/g, '\\"')}"]`
88
+ for (const node of document.querySelectorAll(selector)) {
89
+ if (node.getAttribute("data-timeline-row") === "UserMessage") return node
90
+ if (node.querySelector('[data-slot="user-message-text"]')) return node
91
+ }
92
+ return undefined
93
+ }
85
94
 
86
95
  const diagnostics = (ctx) => {
87
96
  const scroller = findScroller()
@@ -244,20 +253,6 @@ const CSS = `
244
253
  flex-shrink: 0;
245
254
  }
246
255
  .sr-close:hover { opacity: 1; background: var(--surface-base-hover, rgba(127,127,127,.14)); }
247
- .sr-warm {
248
- border: 1px solid rgba(127,127,127,.28);
249
- background: transparent;
250
- color: inherit;
251
- font: inherit;
252
- font-size: 11px;
253
- line-height: 1;
254
- padding: 3px 7px;
255
- border-radius: 5px;
256
- cursor: pointer;
257
- opacity: .6;
258
- flex-shrink: 0;
259
- }
260
- .sr-warm:hover { opacity: 1; background: var(--surface-base-hover, rgba(127,127,127,.14)); }
261
256
  /* the scrollable list; the header stays fixed above it */
262
257
  .sr-list {
263
258
  flex: 1;
@@ -722,17 +717,13 @@ function Rail(props) {
722
717
  setOpen((value) => !value)
723
718
  maybePrewarm()
724
719
  }
725
- const onWarm = () => void prewarm()
726
720
  window.addEventListener(DIAG_EVENT, onDiag)
727
721
  window.addEventListener(OPEN_EVENT, onOpen)
728
- window.addEventListener(WARM_EVENT, onWarm)
729
722
  onCleanup(() => {
730
723
  window.removeEventListener(DIAG_EVENT, onDiag)
731
724
  window.removeEventListener(OPEN_EVENT, onOpen)
732
- window.removeEventListener(WARM_EVENT, onWarm)
733
725
  })
734
- // One-shot confirmation that this window actually runs the rebuilt bundle.
735
- flash(`会话轮次 ${VERSION} 已加载`)
726
+ // No startup toast: the plugin should be invisible when everything works.
736
727
  })
737
728
 
738
729
  // ---- jump ----------------------------------------------------------------
@@ -896,11 +887,11 @@ function Rail(props) {
896
887
  const sc = scroller()
897
888
  const row = findRow(turn.id)
898
889
 
899
- // Fast path: the row is mounted, scroll locally.
890
+ // Fast path: the row is mounted, scroll locally. No toast: the bubble
891
+ // ring is the feedback.
900
892
  if (row && sc) {
901
893
  alignAfterLand(row)
902
894
  pulse(row)
903
- flash("已定位")
904
895
  return
905
896
  }
906
897
 
@@ -929,7 +920,9 @@ function Rail(props) {
929
920
  setJumpElapsed(0)
930
921
  // Pin the panel open even if the pointer has left it meanwhile.
931
922
  setOpen(true)
932
- flash("正在定位…", JUMP_TOTAL_MS)
923
+ // Only say something when the click cannot be served instantly: the target
924
+ // is not in the DOM yet, so the host has to load history first.
925
+ flash(warming() ? "正在预热历史,请稍候…" : "正在加载历史…", 5000)
933
926
  const startedAt = Date.now()
934
927
  const elapsedTimer = setInterval(() => {
935
928
  setJumpElapsed(Math.round((Date.now() - startedAt) / 1000))
@@ -959,21 +952,19 @@ function Rail(props) {
959
952
  }
960
953
  if (token !== jumpToken || sessionID() !== sessionAtJump) return
961
954
  navTo(path, turn.id)
962
- flash(`跳转中 · 已加载 ${chainBase} 页`, JUMP_TOTAL_MS)
963
955
 
964
956
  let chainPages = 0
965
957
  const remaining = Math.min(JUMP_CHAIN_MS, deadline - Date.now())
966
958
  const landed = await waitForRow(turn.id, token, remaining, (pages) => {
967
959
  chainPages = pages
960
+ // Progress is shown in the panel header, not in a toast.
968
961
  setJumpPages(chainBase + pages)
969
- flash(`跳转中 · 已加载 ${chainBase + pages} 页`, JUMP_TOTAL_MS)
970
962
  })
971
963
  chainBase += chainPages
972
964
  if (token !== jumpToken || sessionID() !== sessionAtJump) return
973
965
  if (landed) {
974
966
  alignAfterLand(landed)
975
967
  pulse(landed)
976
- flash("已定位")
977
968
  return
978
969
  }
979
970
  // The host went idle without landing: it exhausted this chain. The
@@ -1002,7 +993,6 @@ function Rail(props) {
1002
993
  // and restore the viewport afterwards. Once the store holds the whole
1003
994
  // history every jump is instant.
1004
995
  const [warming, setWarming] = createSignal(false)
1005
- const [warmPages, setWarmPages] = createSignal(0)
1006
996
  let warmToken = 0
1007
997
  let warmedFor = ""
1008
998
 
@@ -1014,9 +1004,9 @@ function Rail(props) {
1014
1004
  for (const row of sc.querySelectorAll("[data-message-id]")) {
1015
1005
  const rect = row.getBoundingClientRect()
1016
1006
  if (rect.bottom <= scRect.top + 1) continue
1017
- const id = row.getAttribute("data-message-id")
1018
- if (!id) continue
1019
- return { id, offset: rect.top - scRect.top }
1007
+ const mid = row.getAttribute("data-message-id")
1008
+ if (!mid) continue
1009
+ return { el: row, id: mid, offset: rect.top - scRect.top }
1020
1010
  }
1021
1011
  return undefined
1022
1012
  }
@@ -1025,7 +1015,9 @@ function Rail(props) {
1025
1015
  if (!anchor) return
1026
1016
  const sc = scroller()
1027
1017
  if (!sc) return
1028
- const row = findRow(anchor.id)
1018
+ // Prefer the element itself: an assistant row can carry the same message
1019
+ // id, so looking it up by id might land on a different element.
1020
+ const row = anchor.el?.isConnected ? anchor.el : findRow(anchor.id)
1029
1021
  if (!row) return
1030
1022
  const scRect = sc.getBoundingClientRect()
1031
1023
  const delta = row.getBoundingClientRect().top - scRect.top - anchor.offset
@@ -1054,33 +1046,20 @@ function Rail(props) {
1054
1046
  const sid = sessionID()
1055
1047
  if (!path || !oldest || !sid) return
1056
1048
  // Already warm: the oldest turn is mounted, so the store has the history.
1057
- if (warmedFor === sid && findRow(oldest.id)) {
1058
- flash("历史已就绪")
1059
- return
1060
- }
1049
+ if (warmedFor === sid && findRow(oldest.id)) return
1061
1050
  const token = ++warmToken
1062
1051
  const anchor = viewAnchor()
1063
1052
  const sc = scroller()
1064
1053
  const wasAtBottom = sc ? sc.scrollTop + sc.clientHeight >= sc.scrollHeight - 32 : true
1065
1054
  setWarming(true)
1066
- setWarmPages(0)
1067
- flash("正在预热历史…", 120000)
1068
1055
  try {
1069
1056
  navTo(path, oldest.id)
1070
1057
  const deadline = Date.now() + 120000
1071
- let pages = 0
1072
- let lastSig = rowSignature()
1073
1058
  const keep = setInterval(() => restoreView(anchor), 150)
1074
1059
  try {
1075
1060
  while (Date.now() < deadline && token === warmToken) {
1076
1061
  if (findRow(oldest.id)) break
1077
1062
  await sleep(250)
1078
- const sig = rowSignature()
1079
- if (sig !== lastSig) {
1080
- lastSig = sig
1081
- pages++
1082
- setWarmPages(pages)
1083
- }
1084
1063
  }
1085
1064
  } finally {
1086
1065
  clearInterval(keep)
@@ -1093,7 +1072,7 @@ function Rail(props) {
1093
1072
  if (live) live.scrollTop = live.scrollHeight
1094
1073
  }
1095
1074
  warmedFor = sid
1096
- flash(`历史已预热(${pages} 页),现在跳转是瞬时的`)
1075
+ // Silent on success: prewarming is an implementation detail.
1097
1076
  } finally {
1098
1077
  if (token === warmToken) setWarming(false)
1099
1078
  }
@@ -1135,25 +1114,16 @@ function Rail(props) {
1135
1114
  >
1136
1115
  <div class="sr-head">
1137
1116
  <Show
1138
- when={jumping() || warming()}
1117
+ when={jumping()}
1139
1118
  fallback={
1140
1119
  <span class="sr-title">会话轮次{turns().length > 0 ? ` · ${turns().length}` : ""}</span>
1141
1120
  }
1142
1121
  >
1143
1122
  <span class="sr-title sr-progress">
1144
- {jumping()
1145
- ? `跳转中${jumpPages() > 0 ? ` · 已加载 ${jumpPages()} 页` : ""}${jumpElapsed() > 0 ? ` · ${jumpElapsed()}s` : ""}`
1146
- : `预热中${warmPages() > 0 ? ` · ${warmPages()} 页` : ""}`}
1123
+ 跳转中{jumpPages() > 0 ? ` · 已加载 ${jumpPages()} 页` : ""}
1124
+ {jumpElapsed() > 0 ? ` · ${jumpElapsed()}s` : ""}
1147
1125
  </span>
1148
1126
  </Show>
1149
- <button
1150
- type="button"
1151
- class="sr-warm"
1152
- title="预热历史:后台把会话历史加载完,之后跳转瞬时"
1153
- onClick={() => window.dispatchEvent(new Event(WARM_EVENT))}
1154
- >
1155
- 预热
1156
- </button>
1157
1127
  <button
1158
1128
  type="button"
1159
1129
  class="sr-close"