opencode-timeline-gui 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gui-experimental.cjs +3 -3
- package/package.json +1 -1
- package/src/gui.tsx +64 -22
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const r=require("solid-js/web"),
|
|
2
|
-
`).map(m=>m.trim()).find(m=>m.length>0);if(
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const r=require("solid-js/web"),J=require("@opencode/plugin/gui"),a=require("solid-js");var st=r.template("<div class=sr-status>"),$t=r.template('<span class="sr-title sr-progress">'),St=r.template('<svg><path d="M6 3l7 7-3.5.5L7 13 3 9l2.5-2.5z"></svg>',!1,!0,!1),Ct=r.template('<svg><path d="M4 12l-1.5 1.5"></svg>',!1,!0,!1),Et=r.template("<button type=button class=sr-search-clear aria-label=清除搜索>×"),_t=r.template("<div class=sr-search><input type=text class=sr-search-input placeholder=搜索轮次…>"),At=r.template("<div class=sr-foot>"),Mt=r.template('<div class=sr-rail><div class=sr-head><span class=sr-head-actions><button type=button class=sr-iconbtn><svg viewBox="0 0 16 16"fill=none stroke=currentColor stroke-width=1.5 stroke-linecap=round stroke-linejoin=round aria-hidden=true></svg></button><button type=button class=sr-close>×</button></span></div><div class=sr-list tabindex=-1>'),Tt=r.template("<span class=sr-title>会话轮次"),Dt=r.template('<svg><path d="M9.5 2.5l4 4-2.6.9-1.4 3.5-4.4-4.4L8.6 5.1z"></svg>',!1,!0,!1),It=r.template('<svg><path d="M5.1 6.5L2.5 9.1l4.4 4.4 2.6-2.6"></svg>',!1,!0,!1),Lt=r.template("<div class=sr-note>"),Pt=r.template("<span class=sr-badge>"),jt=r.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 ot="v22",_e="message-",it="session-turns:diag",Ae="session-turns:open",Me="session-turns:pin",Rt=40,qt=200,Bt=50,Ot=30,Ft=45e3,zt=2e4,Jt=6e3,Nt=6e3,at=220,Ht=800;let dt=0,Te="",De="",Ie="";const lt=l=>{const o=String(l?.text??"").split(`
|
|
2
|
+
`).map(m=>m.trim()).find(m=>m.length>0);if(o)return o;const c=l?.files?.length??0;return c>0?`[附件 ×${c}]`:"(空消息)"},P=l=>String(l).padStart(2,"0"),Ut=l=>{const o=new Date(l??0),c=new Date;if(o.getFullYear()===c.getFullYear()&&o.getMonth()===c.getMonth()&&o.getDate()===c.getDate())return`${P(o.getHours())}:${P(o.getMinutes())}`;const $=new Date(c);return $.setDate(c.getDate()-1),o.getFullYear()===$.getFullYear()&&o.getMonth()===$.getMonth()&&o.getDate()===$.getDate()?`昨天 ${P(o.getHours())}:${P(o.getMinutes())}`:`${P(o.getMonth()+1)}-${P(o.getDate())} ${P(o.getHours())}:${P(o.getMinutes())}`},pe=l=>new Promise(o=>setTimeout(o,l)),Le=()=>document.querySelector('[data-slot="session-timeline-scroll"] > .scroll-view__viewport')??document.querySelector('[data-slot="session-timeline-scroll"] [data-scrollable]')??(()=>{let o=document.querySelector("[data-message-id]")?.parentElement;for(;o;){const c=getComputedStyle(o);if(/(auto|scroll)/.test(c.overflowY)&&o.scrollHeight>o.clientHeight+1)return o;o=o.parentElement}})(),_=l=>{const o=document.getElementById(`${_e}${l}`);if(o)return o;const c=`[data-message-id="${l.replace(/"/g,'\\"')}"]`;for(const m of document.querySelectorAll(c))if(m.getAttribute("data-timeline-row")==="UserMessage"||m.querySelector('[data-slot="user-message-text"]'))return m},Ce=l=>{const o=Le(),c=[...document.querySelectorAll("[data-message-id]")],m=c[0]?.getAttribute("data-message-id")??"",$=Ie?` · 目标 ${_(Ie)?"在":"不在"} DOM`:"",T=m?` · 行ID ${m.slice(0,20)}`:"";let S="(空)";try{S=l.router.path()||"(空)"}catch{}return`轮次 ${dt} · 容器 ${o?"OK":"缺失"} · DOM ${c.length} 行${$}${T} · 路由 ${S} · hash ${window.location.hash||"(空)"}${De?` · ${De}`:""}${Te?` · ${Te}`:""}`},Wt=l=>{const o=l.getBoundingClientRect().top;let c=o;for(const m of l.querySelectorAll(".sticky, [data-session-title]")){if(!(m instanceof HTMLElement)||getComputedStyle(m).position!=="sticky"&&!m.hasAttribute("data-session-title"))continue;const T=m.getBoundingClientRect();T.height!==0&&(T.top>o+4||(c=Math.max(c,T.bottom)))}return c},Ee=l=>{try{const o=Le();if(!o||!l.isConnected)return;const c=l.querySelector('[data-slot="user-message-text"]')??l,m=Wt(o),$=c.getBoundingClientRect().top-(m+18);Math.abs($)>2&&(o.scrollTop+=$),De=`inset ${Math.round(m)} · delta ${Math.round($)}`}catch{}},fe=l=>{Ee(l),setTimeout(()=>Ee(l),450),setTimeout(()=>Ee(l),1e3)},ct=l=>{let o="";try{o=l.router.path()||""}catch{}return o.startsWith("/")?o:""},Yt=l=>{const o=document.createElement("a");o.setAttribute("href",l),o.setAttribute("replace",""),o.setAttribute("noscroll",""),o.style.display="none",document.body.appendChild(o);try{o.dispatchEvent(new MouseEvent("click",{bubbles:!0,cancelable:!0,button:0,view:window}))}finally{o.remove()}},Gt=`
|
|
3
3
|
.sr-rail {
|
|
4
4
|
position: fixed;
|
|
5
5
|
top: 50%;
|
|
@@ -315,4 +315,4 @@
|
|
|
315
315
|
.sr-turn[data-busy="true"] .sr-dash { animation: none !important; opacity: 1 !important; }
|
|
316
316
|
.sr-flash-ring { animation: none !important; box-shadow: inset 0 0 0 2px rgba(255,170,60,.5) !important; }
|
|
317
317
|
}
|
|
318
|
-
`;function Wt(l){const s=F.usePlugin(),[c,m]=a.createSignal([]),[$,T]=a.createSignal(""),[S,I]=a.createSignal(!1),[lt,ct]=a.createSignal(""),[O,De]=a.createSignal(""),[Q,Ie]=a.createSignal(""),[_,pe]=a.createSignal(!1),[A,X]=a.createSignal(!1),[Le,z]=a.createSignal(0),[Pe,J]=a.createSignal(0),[Gt,N]=a.createSignal(0),[ee,te]=a.createSignal(""),[je,H]=a.createSignal(!1),D=new Map;let fe=0,U=!1,re=!1,ne="",W;const k=()=>l.session()?.id,ge=()=>{const e=[...D.values()].sort((n,i)=>n.created-i.created),t=a.untrack(c);t.length===e.length&&t.every((n,i)=>n===e[i])||(m(e),at=e.length)},qe=e=>{const t=D.get(e.id);t&&t.created===e.time&&t.text===e.text||D.set(e.id,{id:e.id,created:e.time,text:e.text})},dt=async(e,t,n)=>{const i=n?{sessionID:e,type:"user",limit:t,cursor:n}:{sessionID:e,type:"user",limit:t,order:"desc"};return await s.client.message.list(i)},he=async(e,t)=>{const n=++fe;U=!0,t==="full"&&(G="",xe="");let i="";try{let g;const d=t==="full"?Lt:1,v=t==="full"?Pt:jt;let o=t!=="full";for(let u=0;u<d;u++){let f;try{f=await dt(e,v,g)}catch(w){i=String(w?.message??w);break}if(n!==fe)return;o||(D.clear(),o=!0);let h=0;for(const w of f?.data??[])D.has(w.id)||(qe({id:w.id,time:w.time?.created??0,text:ot(w)}),h++);ge();const b=f?.cursor?.next;if(!b||h===0)break;g=b}}finally{U=!1,ne=e,_e=i,ct(D.size===0?i||"暂无用户消息":""),re&&(re=!1,Re()),t==="full"&&!i&&e===a.untrack(k)&&D.size>=15&&setTimeout(()=>V(),1200)}},Re=()=>{W||(W=setTimeout(()=>{W=void 0;const e=a.untrack(k);e&&he(e,"light")},300))},Be=e=>{if(!(!e||e!==a.untrack(k))){if(U){re=!0;return}Re()}},ut=e=>{const t=e?.type;if(t==="message.updated"){const n=e?.data??{},i=n.info;i?.role==="user"&&i?.id&&(qe({id:i.id,time:i.time?.created??0,text:ot(i)}),ge()),Be(n.sessionID)}else if(t==="message.removed"){const n=e?.data??{};n.messageID&&D.delete(n.messageID)&&ge(),Be(n.sessionID)}else if(t==="location.shutdown"){const n=a.untrack(k);n&&ne===n&&he(n,"full")}};a.onMount(()=>{let e=!1,t=0,n;(async()=>{for(;!e;){const d=s.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!e;){const v=await d.next();if(v.done)break;t=0,ut(v.value)}}catch{}try{await d.return?.()}catch{}if(e)break;t++,await de(Math.min(2e3*t,15e3))}})(),a.onCleanup(()=>{e=!0,clearTimeout(n),W&&clearTimeout(W)})}),a.createEffect(()=>{const e=k();e&&a.untrack(()=>{ne!==e&&(fe++,U=!1,re=!1,D.clear(),m([]),T(""),we="",he(e,"full"))})});let se;const j=()=>(se?.isConnected||(se=Te()),se),Oe=new WeakSet;let Y;const pt=()=>Y||(Y=new IntersectionObserver(e=>{e.length!==0&&be()},{rootMargin:"-40px 0px 0px 0px",threshold:0}),Y),ft=()=>{const e=pt();for(const t of document.querySelectorAll("[data-message-id]"))Oe.has(t)||(Oe.add(t),e.observe(t))},gt=()=>{const e=j();if(!e)return;const t=e.querySelectorAll("[data-message-id]");if(t.length===0)return;const n=e.getBoundingClientRect().top+40;for(const i of t)if(i.getBoundingClientRect().bottom>=n){const g=i.getAttribute("data-message-id");g&&g!==$()&&T(g);break}};let me=!1;const be=()=>{me||(me=!0,requestAnimationFrame(()=>{me=!1,gt(),ft()}))};a.onMount(()=>{const e=()=>be();document.addEventListener("scroll",e,!0);const t=setInterval(()=>{document.hidden||be()},4e3);a.onCleanup(()=>{document.removeEventListener("scroll",e,!0),clearInterval(t),Y?.disconnect()})});let ze=0;const oe=(e,t=2600)=>{const n=++ze;De(e),setTimeout(()=>{n===ze&&De("")},t)};a.onMount(()=>{const e=n=>{if(n.key==="Escape"){if(a.untrack(A)){Ue(),n.stopPropagation();return}a.untrack(S)&&(a.untrack(_)?pe(!1):I(!1),n.stopPropagation())}},t=n=>{if(!a.untrack(S)||a.untrack(_))return;(n.composedPath?.()??[]).some(d=>d instanceof Element&&d.closest?.(".sr-rail"))||I(!1)};window.addEventListener("keydown",e,!0),document.addEventListener("pointerdown",t,!0),a.onCleanup(()=>{window.removeEventListener("keydown",e,!0),document.removeEventListener("pointerdown",t,!0)})}),a.onMount(()=>{const e=()=>oe(`${rt} · ${ke(s)}`),t=()=>{I(i=>!i),V()},n=()=>{pe(i=>!i),a.untrack(S)||(I(!0),V())};window.addEventListener(nt,e),window.addEventListener(Ce,t),window.addEventListener(Ee,n),a.onCleanup(()=>{window.removeEventListener(nt,e),window.removeEventListener(Ce,t),window.removeEventListener(Ee,n)})});let we="",C=0,Fe=0,Je="";const ie=e=>{try{const t=e.querySelector('[data-slot="user-message-text"]')??e.querySelector('[data-slot="user-message-body"]')??e;t.classList.remove("sr-flash-ring"),t.offsetWidth,t.classList.add("sr-flash-ring"),setTimeout(()=>t.classList.remove("sr-flash-ring"),1900)}catch{}},Ne=()=>{for(const e of c())if(E(e.id))return e},q=(e,t)=>{Ht(`${e}#${Se}${t}`),we=t},ae=()=>{const t=j()?.querySelectorAll("[data-message-id]")??[];if(t.length===0)return"0";const n=t[0].getAttribute("data-message-id")??"",i=t[t.length-1].getAttribute("data-message-id")??"";return`${t.length}:${n}:${i}`},He=(e,t,n,i)=>new Promise(g=>{let d=!1,v,o,u;const f=Date.now();let b=ae(),w=f,y=0;const p=M=>{if(!d){d=!0;try{v?.disconnect()}catch{}clearInterval(o),clearTimeout(u),g(M??null)}},x=M=>{if(t!==C)return p(null);const Z=M?E(e):document.getElementById(`${Se}${e}`);if(Z)return p(Z);if(!M)return;const Xe=ae(),et=Date.now();if(Xe!==b){b=Xe,w=et,y++,i?.(y);return}const vt=y>0?Ot:zt;et-w>vt&&p(null)},L=E(e);if(L)return p(L);v=new MutationObserver(()=>x(!1)),v.observe(document.documentElement,{childList:!0,subtree:!0}),o=setInterval(()=>x(!0),120),u=setTimeout(()=>p(E(e)),Math.max(400,n))}),Ue=()=>{C++,X(!1),z(0),J(0),N(0),te(""),H(!1),oe("已取消跳转")},We=async e=>{if(A()&&ee()===e.id)return;const t=it(s),n=j(),i=E(e.id);if(i&&n){R()&&(le(),t&&q(t,e.id)),ue(i),ie(i);return}const g=Date.now();if(e.id===Je&&g-Fe<Ft){const h=E(e.id);if(h&&n){ue(h),ie(h);return}}if(Fe=g,Je=e.id,Me=e.id,!t){oe(`无法读取当前路由,无法定位 · ${ke(s)}`);return}const d=++C,v=k();X(!0),te(e.id),z(0),J(0),N(0),H(R()),I(!0);const o=Date.now(),u=setInterval(()=>{J(Math.round((Date.now()-o)/1e3))},500);let f=0;try{if(R()){const b=await He(e.id,d,2e4,w=>z(w));if(d!==C||k()!==v)return;if(b){le(),q(t,e.id),ue(b),ie(b);return}if(R()&&le(),H(!1),d!==C||k()!==v)return}const h=Date.now()+Rt;for(let b=0;b<qt&&Date.now()<h;b++){if(d!==C||k()!==v)return;if(N(b+1),we===e.id){const x=Ne()??c()[0];x&&(q(t,x.id),await de(st))}if(d!==C||k()!==v)return;q(t,e.id);let w=0;const y=Math.min(Bt,h-Date.now()),p=await He(e.id,d,y,x=>{w=x,z(f+x)});if(f+=w,d!==C||k()!==v)return;if(p){ue(p),ie(p);return}}d===C&&oe(`未能定位 · ${ke(s)}`)}finally{clearInterval(u),d===C&&(X(!1),z(0),J(0),N(0),te(""),H(!1))}},[R,ve]=a.createSignal(!1);let B=0,G="",xe="";const ht=()=>{const e=j();if(!e)return;const t=e.getBoundingClientRect();for(const n of e.querySelectorAll("[data-message-id]")){const i=n.getBoundingClientRect();if(i.bottom<=t.top+1)continue;const g=n.getAttribute("data-message-id");if(g)return{el:n,id:g,offset:i.top-t.top}}},Ye=e=>{if(!e)return;const t=j();if(!t)return;const n=e.el?.isConnected?e.el:E(e.id);if(!n)return;const i=t.getBoundingClientRect(),g=n.getBoundingClientRect().top-i.top-e.offset;Math.abs(g)>2&&(t.scrollTop+=g)},le=()=>{B++,ve(!1)},V=()=>{const e=k();!e||R()||G!==e&&(c().length<15||mt())},mt=async()=>{if(R())return;const e=it(s),t=c()[0],n=k();if(!e||!t||!n||G===n&&E(t.id))return;const i=++B,g=ht(),d=j(),v=d?d.scrollTop+d.clientHeight>=d.scrollHeight-32:!0;ve(!0);try{q(e,t.id);const o=Date.now()+6e4;let u=ae(),f=Date.now(),h=0;const b=setInterval(()=>Ye(g),150);try{for(;Date.now()<o&&i===B&&!(E(t.id)||(await de(250),i!==B));){const y=Date.now(),p=ae();if(p!==u)u=p,f=y,h=0;else if(y-f>5e3){if(h>=2)break;const x=Ne();if(x&&x.id!==t.id&&(q(e,x.id),await de(st),i!==B))break;q(e,t.id),f=Date.now(),h++}}}finally{clearInterval(b)}if(i!==B)return;const w=!!E(t.id);if(Ye(g),v){const y=j();y&&(y.scrollTop=y.scrollHeight)}w?G=n:xe=n}finally{i===B&&ve(!1)}};let Ge=a.untrack(k);a.createEffect(()=>{const e=k();e!==Ge&&(Ge=e,le(),C++,X(!1),z(0),J(0),N(0),te(""),H(!1))}),a.createEffect(()=>{const e=k(),t=c().length,n=R();if(!e||t<15||n||G===e||xe===e||U||ne!==e)return;const i=setTimeout(()=>V(),1200);a.onCleanup(()=>clearTimeout(i))});const K=a.createMemo(()=>{const e=c(),t=Q().trim().toLowerCase();return t?e.filter(n=>n.text.toLowerCase().includes(t)):e}),[ce,Ve]=a.createSignal(-1),Ke=()=>Ve(-1),Ze=e=>{const t=K();if(t.length===0)return;let n=ce();if(n<0){const i=$();n=t.findIndex(g=>g.id===i),n<0&&(n=e>0?-1:0)}n=Math.min(t.length-1,Math.max(0,n+e)),Ve(n),requestAnimationFrame(()=>{document.querySelector(`.sr-list .sr-turn[data-index="${n}"]`)?.scrollIntoView({block:"nearest"})})},bt=()=>{const e=K()[ce()];e&&We(e)};let ye;const Qe=e=>{e.key==="ArrowDown"?(e.preventDefault(),e.stopPropagation(),Ze(1)):e.key==="ArrowUp"?(e.preventDefault(),e.stopPropagation(),Ze(-1)):e.key==="Enter"&&ce()>=0&&(e.preventDefault(),e.stopPropagation(),bt())};a.createEffect(()=>{A()&&ee()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const wt=[r.createComponent(a.Show,{get when(){return O()},get children(){var e=tt();return r.insert(e,O),e}}),(()=>{var e=Et(),t=e.firstChild,n=t.firstChild,i=n.firstChild,g=i.firstChild,d=i.nextSibling,v=t.nextSibling;return e.addEventListener("mouseleave",()=>{!A()&&!_()&&I(!1)}),e.addEventListener("mouseenter",()=>{I(!0),V()}),r.insert(t,r.createComponent(a.Show,{get when(){return A()},get fallback(){return(()=>{var o=_t();return o.firstChild,r.insert(o,(()=>{var u=r.memo(()=>c().length>0);return()=>u()?` · ${c().length}`:""})(),null),r.insert(o,r.createComponent(a.Show,{get when(){return Q().trim()},get children(){return[" · 匹配 ",r.memo(()=>K().length)]}}),null),o})()},get children(){var o=xt();return r.insert(o,()=>je()?"预热中 · 稍等一下":"加载历史中",null),r.insert(o,(()=>{var u=r.memo(()=>Le()>0);return()=>u()?` · ${Le()} 页`:""})(),null),r.insert(o,(()=>{var u=r.memo(()=>Pe()>0);return()=>u()?` · ${Pe()}s`:""})(),null),o}}),n),i.$$click=()=>pe(!_()),r.insert(g,r.createComponent(a.Show,{get when(){return _()},get fallback(){return[At(),Mt()]},get children(){return[yt(),kt()]}})),d.$$click=()=>{A()?Ue():I(!1)},r.insert(e,r.createComponent(a.Show,{get when(){return c().length>=8},get children(){var o=St(),u=o.firstChild;u.$$keydown=Qe,u.$$input=h=>{Ie(h.currentTarget.value),Ke()};var f=ye;return typeof f=="function"?r.use(f,u):ye=u,r.insert(o,r.createComponent(a.Show,{get when(){return Q()},get children(){var h=$t();return h.$$click=()=>{Ie(""),Ke(),ye?.focus()},h}}),null),r.effect(()=>u.value=Q()),o}}),v),v.$$keydown=Qe,r.insert(v,r.createComponent(a.Show,{get when(){return K().length>0},get fallback(){return(()=>{var o=Tt();return r.insert(o,()=>lt()||`… ${rt}`),o})()},get children(){return r.createComponent(a.For,{get each(){return K()},children:(o,u)=>(()=>{var f=It(),h=f.firstChild,b=h.firstChild;b.firstChild;var w=b.nextSibling,y=h.nextSibling;return f.$$click=()=>{We(o)},r.insert(b,()=>c().indexOf(o)+1,null),r.insert(w,()=>Jt(o.created)),r.insert(h,()=>o.text,null),r.insert(f,r.createComponent(a.Show,{get when(){return r.memo(()=>!!A())()&&ee()===o.id},get children(){var p=Dt();return r.insert(p,()=>je()?"稍等":"加载中"),p}}),y),r.effect(p=>{var x=$()===o.id?"true":"false",L=ce()===u()?"true":"false",M=A()&&ee()===o.id?"true":"false",Z=u();return x!==p.e&&r.setAttribute(f,"data-active",p.e=x),L!==p.t&&r.setAttribute(f,"data-cursor",p.t=L),M!==p.a&&r.setAttribute(f,"data-busy",p.a=M),Z!==p.o&&r.setAttribute(f,"data-index",p.o=Z),p},{e:void 0,t:void 0,a:void 0,o:void 0}),f})()})}})),r.insert(e,r.createComponent(a.Show,{get when(){return r.memo(()=>!!S())()&&O()},get children(){var o=Ct();return r.insert(o,O),o}}),null),r.effect(o=>{var u=S()?"true":"false",f=A()?"true":"false",h=_()?"true":"false",b=S()?"dialog":void 0,w=S()?"会话轮次":void 0,y=_()?"true":"false",p=_()?"取消固定 (⌘⌥P)":"固定面板 (⌘⌥P)",x=_()?"取消固定面板":"固定面板",L=_()?"true":"false",M=A()?"取消跳转 (Esc)":"关闭 (Esc)";return u!==o.e&&r.setAttribute(e,"data-open",o.e=u),f!==o.t&&r.setAttribute(e,"data-jumping",o.t=f),h!==o.a&&r.setAttribute(e,"data-pinned",o.a=h),b!==o.o&&r.setAttribute(e,"role",o.o=b),w!==o.i&&r.setAttribute(e,"aria-label",o.i=w),y!==o.n&&r.setAttribute(i,"data-on",o.n=y),p!==o.s&&r.setAttribute(i,"title",o.s=p),x!==o.h&&r.setAttribute(i,"aria-label",o.h=x),L!==o.r&&r.setAttribute(i,"aria-pressed",o.r=L),M!==o.d&&r.setAttribute(d,"title",o.d=M),o},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0,n:void 0,s:void 0,h:void 0,r:void 0,d:void 0}),e})(),r.createComponent(a.Show,{get when(){return r.memo(()=>!S())()&&O()},get children(){var e=tt();return r.insert(e,O),e}})];return r.createComponent(r.Portal,{get mount(){return document.body},children:wt})}const Yt=F.Plugin.define({i18n:{en:{"command.jump":"Session turns: show / jump","command.jump.description":"Reveal the turn rail; click a turn to jump to that message","command.pin":"Session turns: pin panel","command.pin.description":"Toggle pinning the turn rail open (⌘⌥P)"}},setup(l){l.add(F.Style,Ut),l.add(F.Slot,{at:"window.bottom",render:()=>r.createComponent(a.Show,{get when(){return l.sessions.current()},get children(){return r.createComponent(Wt,{session:()=>l.sessions.current()})}})}),l.add(F.Command,()=>({id:"jump",title:l.t("command.jump"),description:l.t("command.jump.description"),section:"navigation",bind:"mod+alt+r",slash:{name:"turns"},run:()=>{window.dispatchEvent(new Event(Ce))}})),l.add(F.Command,()=>({id:"pin",title:l.t("command.pin"),description:l.t("command.pin.description"),section:"navigation",bind:"mod+alt+p",slash:{name:"turns-pin"},run:()=>{window.dispatchEvent(new Event(Ee))}}))}});r.delegateEvents(["click","input","keydown"]);exports.default=Yt;
|
|
318
|
+
`;function Vt(l){const o=J.usePlugin(),[c,m]=a.createSignal([]),[$,T]=a.createSignal(""),[S,I]=a.createSignal(!1),[ut,pt]=a.createSignal(""),[O,Pe]=a.createSignal(""),[Q,je]=a.createSignal(""),[A,he]=a.createSignal(!1),[M,X]=a.createSignal(!1),[Re,F]=a.createSignal(0),[qe,N]=a.createSignal(0),[Zt,H]=a.createSignal(0),[ee,te]=a.createSignal(""),[Be,U]=a.createSignal(!1),D=new Map;let ge=0,W=!1,re=!1,ne="",Y;const k=()=>l.session()?.id,me=()=>{const e=[...D.values()].sort((n,i)=>n.created-i.created),t=a.untrack(c);t.length===e.length&&t.every((n,i)=>n===e[i])||(m(e),dt=e.length)},Oe=e=>{const t=D.get(e.id);t&&t.created===e.time&&t.text===e.text||D.set(e.id,{id:e.id,created:e.time,text:e.text})},ft=async(e,t,n)=>{const i=n?{sessionID:e,type:"user",limit:t,cursor:n}:{sessionID:e,type:"user",limit:t,order:"desc"};return await o.client.message.list(i)},be=async(e,t)=>{const n=++ge;W=!0,t==="full"&&(z="",$e="");let i="";try{let p;const f=t==="full"?Rt:1,x=t==="full"?qt:Bt;let s=t!=="full";for(let d=0;d<f;d++){let u;try{u=await ft(e,x,p)}catch(w){i=String(w?.message??w);break}if(n!==ge)return;s||(D.clear(),s=!0);let h=0;for(const w of u?.data??[])D.has(w.id)||(Oe({id:w.id,time:w.time?.created??0,text:lt(w)}),h++);me();const b=u?.cursor?.next;if(!b||h===0)break;p=b}}finally{W=!1,ne=e,Te=i,pt(D.size===0?i||"暂无用户消息":""),re&&(re=!1,Fe()),t==="full"&&!i&&e===a.untrack(k)&&D.size>=1&&setTimeout(()=>V(),400)}},Fe=()=>{Y||(Y=setTimeout(()=>{Y=void 0;const e=a.untrack(k);e&&be(e,"light")},300))},ze=e=>{if(!(!e||e!==a.untrack(k))){if(W){re=!0;return}Fe()}},ht=e=>{const t=e?.type;if(t==="message.updated"){const n=e?.data??{},i=n.info;i?.role==="user"&&i?.id&&(Oe({id:i.id,time:i.time?.created??0,text:lt(i)}),me()),ze(n.sessionID)}else if(t==="message.removed"){const n=e?.data??{};n.messageID&&D.delete(n.messageID)&&me(),ze(n.sessionID)}else if(t==="location.shutdown"){const n=a.untrack(k);n&&ne===n&&be(n,"full")}};a.onMount(()=>{let e=!1,t=0,n;(async()=>{for(;!e;){const f=o.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!e;){const x=await f.next();if(x.done)break;t=0,ht(x.value)}}catch{}try{await f.return?.()}catch{}if(e)break;t++,await pe(Math.min(2e3*t,15e3))}})(),a.onCleanup(()=>{e=!0,clearTimeout(n),Y&&clearTimeout(Y)})}),a.createEffect(()=>{const e=k();e&&a.untrack(()=>{ne!==e&&(ge++,W=!1,re=!1,D.clear(),m([]),T(""),xe="",be(e,"full"))})});let se;const j=()=>(se?.isConnected||(se=Le()),se),Je=new WeakSet;let G;const gt=()=>G||(G=new IntersectionObserver(e=>{e.length!==0&&ve()},{rootMargin:"-40px 0px 0px 0px",threshold:0}),G),mt=()=>{const e=gt();for(const t of document.querySelectorAll("[data-message-id]"))Je.has(t)||(Je.add(t),e.observe(t))},bt=()=>{const e=j();if(!e)return;const t=e.querySelectorAll("[data-message-id]");if(t.length===0)return;const n=e.getBoundingClientRect().top+40;for(const i of t)if(i.getBoundingClientRect().bottom>=n){const p=i.getAttribute("data-message-id");p&&p!==$()&&T(p);break}};let we=!1;const ve=()=>{we||(we=!0,requestAnimationFrame(()=>{we=!1,bt(),mt()}))};a.onMount(()=>{const e=()=>ve();document.addEventListener("scroll",e,!0);const t=setInterval(()=>{document.hidden||ve()},4e3);a.onCleanup(()=>{document.removeEventListener("scroll",e,!0),clearInterval(t),G?.disconnect()})});let Ne=0;const oe=(e,t=2600)=>{const n=++Ne;Pe(e),setTimeout(()=>{n===Ne&&Pe("")},t)};a.onMount(()=>{const e=n=>{if(n.key==="Escape"){if(a.untrack(M)){Ye(),n.stopPropagation();return}a.untrack(S)&&(a.untrack(A)?he(!1):I(!1),n.stopPropagation())}},t=n=>{if(!a.untrack(S)||a.untrack(A))return;(n.composedPath?.()??[]).some(f=>f instanceof Element&&f.closest?.(".sr-rail"))||I(!1)};window.addEventListener("keydown",e,!0),document.addEventListener("pointerdown",t,!0),a.onCleanup(()=>{window.removeEventListener("keydown",e,!0),document.removeEventListener("pointerdown",t,!0)})}),a.onMount(()=>{const e=()=>oe(`${ot} · ${Ce(o)}`),t=()=>{I(i=>!i),V()},n=()=>{he(i=>!i),a.untrack(S)||(I(!0),V())};window.addEventListener(it,e),window.addEventListener(Ae,t),window.addEventListener(Me,n),a.onCleanup(()=>{window.removeEventListener(it,e),window.removeEventListener(Ae,t),window.removeEventListener(Me,n)})});let xe="",C=0,He=0,Ue="";const ie=e=>{try{const t=e.querySelector('[data-slot="user-message-text"]')??e.querySelector('[data-slot="user-message-body"]')??e;t.classList.remove("sr-flash-ring"),t.offsetWidth,t.classList.add("sr-flash-ring"),setTimeout(()=>t.classList.remove("sr-flash-ring"),1900)}catch{}},ye=()=>{for(const e of c())if(_(e.id))return e},L=(e,t)=>{Yt(`${e}#${_e}${t}`),xe=t},ae=()=>{const t=j()?.querySelectorAll("[data-message-id]")??[];if(t.length===0)return"0";const n=t[0].getAttribute("data-message-id")??"",i=t[t.length-1].getAttribute("data-message-id")??"";return`${t.length}:${n}:${i}`},We=(e,t,n,i,p)=>new Promise(f=>{let x=!1,s,d,u;const h=Date.now(),b=ae();let w=b,y=h,g=0,v=!1;const B=4e3,E=de=>{if(!x){x=!0;try{s?.disconnect()}catch{}clearInterval(d),clearTimeout(u),f(de??null)}},Z=de=>{if(t!==C)return E(null);const rt=de?_(e):document.getElementById(`${_e}${e}`);if(rt)return E(rt);if(!de)return;const nt=ae(),ue=Date.now();if(nt!==w){w=nt,y=ue,g++,i?.(g);return}const kt=g>0?Jt:Nt;if(!v&&p&&b==="0"&&ue-y>B){v=!0,y=ue,p();return}ue-y>kt&&E(null)},tt=_(e);if(tt)return E(tt);s=new MutationObserver(()=>Z(!1)),s.observe(document.documentElement,{childList:!0,subtree:!0}),d=setInterval(()=>Z(!0),120),u=setTimeout(()=>E(_(e)),Math.max(400,n))}),Ye=()=>{C++,X(!1),F(0),N(0),H(0),te(""),U(!1),oe("已取消跳转")},Ge=async e=>{if(M()&&ee()===e.id)return;const t=ct(o),n=j(),i=_(e.id);if(i&&n){R()&&(le(),t&&L(t,e.id)),fe(i),ie(i);return}const p=Date.now();if(e.id===Ue&&p-He<Ht){const h=_(e.id);if(h&&n){fe(h),ie(h);return}}if(He=p,Ue=e.id,Ie=e.id,!t){oe(`无法读取当前路由,无法定位 · ${Ce(o)}`);return}const f=++C,x=k();X(!0),te(e.id),F(0),N(0),H(0),U(R()),I(!0);const s=Date.now(),d=setInterval(()=>{N(Math.round((Date.now()-s)/1e3))},500);let u=0;try{if(R()){const b=await We(e.id,f,2e4,w=>F(w));if(f!==C||k()!==x)return;if(b){le(),L(t,e.id),fe(b),ie(b);return}if(R()&&le(),U(!1),f!==C||k()!==x)return}const h=Date.now()+Ft;for(let b=0;b<Ot&&Date.now()<h;b++){if(f!==C||k()!==x)return;if(H(b+1),xe===e.id){const v=ye()??c()[0];v&&(L(t,v.id),await pe(at))}if(f!==C||k()!==x)return;L(t,e.id);let w=0;const y=Math.min(zt,h-Date.now()),g=await We(e.id,f,y,v=>{w=v,F(u+v)},()=>{const v=ye()??c()[0];v&&v.id!==e.id&&L(t,v.id)});if(u+=w,f!==C||k()!==x)return;if(g){fe(g),ie(g);return}}f===C&&oe(`未能定位 · ${Ce(o)}`)}finally{clearInterval(d),f===C&&(X(!1),F(0),N(0),H(0),te(""),U(!1))}},[R,ke]=a.createSignal(!1);let q=0,z="",$e="";const wt=()=>{const e=j();if(!e)return;const t=e.getBoundingClientRect();for(const n of e.querySelectorAll("[data-message-id]")){const i=n.getBoundingClientRect();if(i.bottom<=t.top+1)continue;const p=n.getAttribute("data-message-id");if(p)return{el:n,id:p,offset:i.top-t.top}}},Ve=e=>{if(!e)return;const t=j();if(!t)return;const n=e.el?.isConnected?e.el:_(e.id);if(!n)return;const i=t.getBoundingClientRect(),p=n.getBoundingClientRect().top-i.top-e.offset;Math.abs(p)>2&&(t.scrollTop+=p)},le=()=>{q++,ke(!1)},V=()=>{const e=k();!e||R()||z!==e&&(c().length<1||vt())},vt=async()=>{if(R())return;const e=ct(o),t=c()[0],n=k();if(!e||!t||!n)return;if(_(t.id)){z=n;return}if(z===n)return;const i=++q,p=wt(),f=j(),x=f?f.scrollTop+f.clientHeight>=f.scrollHeight-32:!0;ke(!0);try{L(e,t.id);const s=Date.now()+3e4;let d=ae(),u=Date.now(),h=0;const b=setInterval(()=>Ve(p),150);try{for(;Date.now()<s&&i===q&&!(_(t.id)||(await pe(250),i!==q));){const y=Date.now(),g=ae();if(g!==d)d=g,u=y,h=0;else if(y-u>3e3){if(h>=2)break;const v=ye();if(v&&v.id!==t.id&&(L(e,v.id),await pe(at),i!==q))break;L(e,t.id),u=Date.now(),h++}}}finally{clearInterval(b)}if(i!==q)return;const w=!!_(t.id);if(Ve(p),x){const y=j();y&&(y.scrollTop=y.scrollHeight)}w?z=n:$e=n}finally{i===q&&ke(!1)}};let Ke=a.untrack(k);a.createEffect(()=>{const e=k();e!==Ke&&(Ke=e,le(),C++,X(!1),F(0),N(0),H(0),te(""),U(!1))}),a.createEffect(()=>{const e=k(),t=c().length,n=R();if(!e||t<1||n||z===e||$e===e||W||ne!==e)return;const i=setTimeout(()=>V(),400);a.onCleanup(()=>clearTimeout(i))});const K=a.createMemo(()=>{const e=c(),t=Q().trim().toLowerCase();return t?e.filter(n=>n.text.toLowerCase().includes(t)):e}),[ce,Ze]=a.createSignal(-1),Qe=()=>Ze(-1),Xe=e=>{const t=K();if(t.length===0)return;let n=ce();if(n<0){const i=$();n=t.findIndex(p=>p.id===i),n<0&&(n=e>0?-1:0)}n=Math.min(t.length-1,Math.max(0,n+e)),Ze(n),requestAnimationFrame(()=>{document.querySelector(`.sr-list .sr-turn[data-index="${n}"]`)?.scrollIntoView({block:"nearest"})})},xt=()=>{const e=K()[ce()];e&&Ge(e)};let Se;const et=e=>{e.key==="ArrowDown"?(e.preventDefault(),e.stopPropagation(),Xe(1)):e.key==="ArrowUp"?(e.preventDefault(),e.stopPropagation(),Xe(-1)):e.key==="Enter"&&ce()>=0&&(e.preventDefault(),e.stopPropagation(),xt())};a.createEffect(()=>{M()&&ee()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const yt=[r.createComponent(a.Show,{get when(){return O()},get children(){var e=st();return r.insert(e,O),e}}),(()=>{var e=Mt(),t=e.firstChild,n=t.firstChild,i=n.firstChild,p=i.firstChild,f=i.nextSibling,x=t.nextSibling;return e.addEventListener("mouseleave",()=>{!M()&&!A()&&I(!1)}),e.addEventListener("mouseenter",()=>{I(!0),V()}),r.insert(t,r.createComponent(a.Show,{get when(){return M()},get fallback(){return(()=>{var s=Tt();return s.firstChild,r.insert(s,(()=>{var d=r.memo(()=>c().length>0);return()=>d()?` · ${c().length}`:""})(),null),r.insert(s,r.createComponent(a.Show,{get when(){return Q().trim()},get children(){return[" · 匹配 ",r.memo(()=>K().length)]}}),null),s})()},get children(){var s=$t();return r.insert(s,()=>Be()?"预热中 · 稍等一下":"加载历史中",null),r.insert(s,(()=>{var d=r.memo(()=>Re()>0);return()=>d()?` · ${Re()} 页`:""})(),null),r.insert(s,(()=>{var d=r.memo(()=>qe()>0);return()=>d()?` · ${qe()}s`:""})(),null),s}}),n),i.$$click=()=>he(!A()),r.insert(p,r.createComponent(a.Show,{get when(){return A()},get fallback(){return[Dt(),It()]},get children(){return[St(),Ct()]}})),f.$$click=()=>{M()?Ye():I(!1)},r.insert(e,r.createComponent(a.Show,{get when(){return c().length>=8},get children(){var s=_t(),d=s.firstChild;d.$$keydown=et,d.$$input=h=>{je(h.currentTarget.value),Qe()};var u=Se;return typeof u=="function"?r.use(u,d):Se=d,r.insert(s,r.createComponent(a.Show,{get when(){return Q()},get children(){var h=Et();return h.$$click=()=>{je(""),Qe(),Se?.focus()},h}}),null),r.effect(()=>d.value=Q()),s}}),x),x.$$keydown=et,r.insert(x,r.createComponent(a.Show,{get when(){return K().length>0},get fallback(){return(()=>{var s=Lt();return r.insert(s,()=>ut()||`… ${ot}`),s})()},get children(){return r.createComponent(a.For,{get each(){return K()},children:(s,d)=>(()=>{var u=jt(),h=u.firstChild,b=h.firstChild;b.firstChild;var w=b.nextSibling,y=h.nextSibling;return u.$$click=()=>{Ge(s)},r.insert(b,()=>c().indexOf(s)+1,null),r.insert(w,()=>Ut(s.created)),r.insert(h,()=>s.text,null),r.insert(u,r.createComponent(a.Show,{get when(){return r.memo(()=>!!M())()&&ee()===s.id},get children(){var g=Pt();return r.insert(g,()=>Be()?"稍等":"加载中"),g}}),y),r.effect(g=>{var v=$()===s.id?"true":"false",B=ce()===d()?"true":"false",E=M()&&ee()===s.id?"true":"false",Z=d();return v!==g.e&&r.setAttribute(u,"data-active",g.e=v),B!==g.t&&r.setAttribute(u,"data-cursor",g.t=B),E!==g.a&&r.setAttribute(u,"data-busy",g.a=E),Z!==g.o&&r.setAttribute(u,"data-index",g.o=Z),g},{e:void 0,t:void 0,a:void 0,o:void 0}),u})()})}})),r.insert(e,r.createComponent(a.Show,{get when(){return r.memo(()=>!!S())()&&O()},get children(){var s=At();return r.insert(s,O),s}}),null),r.effect(s=>{var d=S()?"true":"false",u=M()?"true":"false",h=A()?"true":"false",b=S()?"dialog":void 0,w=S()?"会话轮次":void 0,y=A()?"true":"false",g=A()?"取消固定 (⌘⌥P)":"固定面板 (⌘⌥P)",v=A()?"取消固定面板":"固定面板",B=A()?"true":"false",E=M()?"取消跳转 (Esc)":"关闭 (Esc)";return d!==s.e&&r.setAttribute(e,"data-open",s.e=d),u!==s.t&&r.setAttribute(e,"data-jumping",s.t=u),h!==s.a&&r.setAttribute(e,"data-pinned",s.a=h),b!==s.o&&r.setAttribute(e,"role",s.o=b),w!==s.i&&r.setAttribute(e,"aria-label",s.i=w),y!==s.n&&r.setAttribute(i,"data-on",s.n=y),g!==s.s&&r.setAttribute(i,"title",s.s=g),v!==s.h&&r.setAttribute(i,"aria-label",s.h=v),B!==s.r&&r.setAttribute(i,"aria-pressed",s.r=B),E!==s.d&&r.setAttribute(f,"title",s.d=E),s},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0,n:void 0,s:void 0,h:void 0,r:void 0,d:void 0}),e})(),r.createComponent(a.Show,{get when(){return r.memo(()=>!S())()&&O()},get children(){var e=st();return r.insert(e,O),e}})];return r.createComponent(r.Portal,{get mount(){return document.body},children:yt})}const Kt=J.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","command.pin":"Session turns: pin panel","command.pin.description":"Toggle pinning the turn rail open (⌘⌥P)"}},setup(l){l.add(J.Style,Gt),l.add(J.Slot,{at:"window.bottom",render:()=>r.createComponent(a.Show,{get when(){return l.sessions.current()},get children(){return r.createComponent(Vt,{session:()=>l.sessions.current()})}})}),l.add(J.Command,()=>({id:"jump",title:l.t("command.jump"),description:l.t("command.jump.description"),section:"navigation",bind:"mod+alt+r",slash:{name:"turns"},run:()=>{window.dispatchEvent(new Event(Ae))}})),l.add(J.Command,()=>({id:"pin",title:l.t("command.pin"),description:l.t("command.pin.description"),section:"navigation",bind:"mod+alt+p",slash:{name:"turns-pin"},run:()=>{window.dispatchEvent(new Event(Me))}}))}});r.delegateEvents(["click","input","keydown"]);exports.default=Kt;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-timeline-gui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "OpenCode Desktop 插件:在会话右缘列出每一轮用户提问,点击即可跳转到对应消息。A right-edge rail of user turns; click one to jump to that message. (formerly opencode-session-turns)",
|
|
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 = "
|
|
9
|
+
const VERSION = "v22"
|
|
10
10
|
const HASH_PREFIX = "message-"
|
|
11
11
|
const DIAG_EVENT = "session-turns:diag"
|
|
12
12
|
const OPEN_EVENT = "session-turns:open"
|
|
@@ -24,12 +24,15 @@ const LIGHT_PAGE_SIZE = 50
|
|
|
24
24
|
// to 12 times on its own. The plugin therefore does NOT poll the hash: it
|
|
25
25
|
// sets it once and waits. JUMP_CHAIN_MS is how long a single chain may run,
|
|
26
26
|
// JUMP_IDLE_MS how long the host may stay completely idle before the plugin
|
|
27
|
-
// assumes the chain died and starts a new one.
|
|
27
|
+
// assumes the chain died and starts a new one. Efficiency-first: the host
|
|
28
|
+
// pages with a 2s cooldown, so 6s of total silence means the seek was
|
|
29
|
+
// dropped (its machine starts inactive and ignores early seeks) - bounce
|
|
30
|
+
// and re-engage instead of waiting longer.
|
|
28
31
|
const JUMP_ROUNDS = 30
|
|
29
32
|
const JUMP_TOTAL_MS = 45000
|
|
30
33
|
const JUMP_CHAIN_MS = 20000
|
|
31
|
-
const JUMP_IDLE_MS =
|
|
32
|
-
const JUMP_FIRST_IDLE_MS =
|
|
34
|
+
const JUMP_IDLE_MS = 6000
|
|
35
|
+
const JUMP_FIRST_IDLE_MS = 6000
|
|
33
36
|
const JUMP_TOGGLE_DWELL_MS = 220
|
|
34
37
|
const SAME_TARGET_WINDOW_MS = 800
|
|
35
38
|
|
|
@@ -644,10 +647,12 @@ function Rail(props) {
|
|
|
644
647
|
pendingBump = false
|
|
645
648
|
scheduleLight()
|
|
646
649
|
}
|
|
647
|
-
// Prewarm
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
650
|
+
// Prewarm EVERY session as soon as its full turn list has loaded - the
|
|
651
|
+
// host's seek machine drops seeks issued before the timeline is ready,
|
|
652
|
+
// and an 8-turn session used to hit exactly that window on first click
|
|
653
|
+
// (12s of dead air). Short sessions cost at most one extra host page.
|
|
654
|
+
if (mode === "full" && !error && id === untrack(sessionID) && cache.size >= 1) {
|
|
655
|
+
setTimeout(() => maybePrewarm(), 400)
|
|
651
656
|
}
|
|
652
657
|
}
|
|
653
658
|
}
|
|
@@ -973,7 +978,13 @@ function Rail(props) {
|
|
|
973
978
|
// does NOT resolve just because rows appeared: the host may still be
|
|
974
979
|
// fetching the rest of the history for this target, and interrupting it
|
|
975
980
|
// (by toggling the hash) is what made deep jumps crawl one page per click.
|
|
976
|
-
|
|
981
|
+
//
|
|
982
|
+
// onKick covers the cold-start case: the host's seek machine starts
|
|
983
|
+
// inactive and DROPS a seek issued before the timeline is ready (worst on
|
|
984
|
+
// short sessions, which never used to prewarm). When nothing at all has
|
|
985
|
+
// happened for KICK_AFTER_MS, it is called once to bounce the hash and
|
|
986
|
+
// re-engage the host; then the normal idle limits apply.
|
|
987
|
+
const waitForRow = (messageID, token, timeout, onPage, onKick) =>
|
|
977
988
|
new Promise((resolve) => {
|
|
978
989
|
let done = false
|
|
979
990
|
let observer
|
|
@@ -984,6 +995,8 @@ function Rail(props) {
|
|
|
984
995
|
let lastSig = baseline
|
|
985
996
|
let lastChange = started
|
|
986
997
|
let pages = 0
|
|
998
|
+
let kicked = false
|
|
999
|
+
const KICK_AFTER_MS = 4000
|
|
987
1000
|
const finish = (row) => {
|
|
988
1001
|
if (done) return
|
|
989
1002
|
done = true
|
|
@@ -1015,6 +1028,14 @@ function Rail(props) {
|
|
|
1015
1028
|
// patient - the initial fetch can take seconds, and bouncing the hash
|
|
1016
1029
|
// then would abort it.
|
|
1017
1030
|
const idleLimit = pages > 0 ? JUMP_IDLE_MS : JUMP_FIRST_IDLE_MS
|
|
1031
|
+
// Cold start, nothing has EVER changed: a dropped seek. Kick the host
|
|
1032
|
+
// once instead of burning the whole first-idle budget on dead air.
|
|
1033
|
+
if (!kicked && onKick && baseline === "0" && now - lastChange > KICK_AFTER_MS) {
|
|
1034
|
+
kicked = true
|
|
1035
|
+
lastChange = now
|
|
1036
|
+
onKick()
|
|
1037
|
+
return
|
|
1038
|
+
}
|
|
1018
1039
|
if (now - lastChange > idleLimit) finish(null)
|
|
1019
1040
|
}
|
|
1020
1041
|
const existing = findRow(messageID)
|
|
@@ -1104,6 +1125,8 @@ function Rail(props) {
|
|
|
1104
1125
|
// seek: just wait for the row to mount, then land. The header reads
|
|
1105
1126
|
// "预热中 · 稍等一下" while we wait.
|
|
1106
1127
|
if (warming()) {
|
|
1128
|
+
// No cold-start kick here: the running prewarm owns the hash and has
|
|
1129
|
+
// its own kick loop; competing toggles would fight it.
|
|
1107
1130
|
const landed = await waitForRow(turn.id, token, 20000, (pages) => setJumpPages(pages))
|
|
1108
1131
|
if (token !== jumpToken || sessionID() !== sessionAtJump) return
|
|
1109
1132
|
if (landed) {
|
|
@@ -1151,11 +1174,25 @@ function Rail(props) {
|
|
|
1151
1174
|
|
|
1152
1175
|
let chainPages = 0
|
|
1153
1176
|
const remaining = Math.min(JUMP_CHAIN_MS, deadline - Date.now())
|
|
1154
|
-
const landed = await waitForRow(
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1177
|
+
const landed = await waitForRow(
|
|
1178
|
+
turn.id,
|
|
1179
|
+
token,
|
|
1180
|
+
remaining,
|
|
1181
|
+
(pages) => {
|
|
1182
|
+
chainPages = pages
|
|
1183
|
+
// Progress is shown in the panel header, not in a toast.
|
|
1184
|
+
setJumpPages(chainBase + pages)
|
|
1185
|
+
},
|
|
1186
|
+
// Cold start: the host dropped our seek (its machine activates only
|
|
1187
|
+
// after the timeline is ready). Bounce the hash through a mounted
|
|
1188
|
+
// turn to re-engage it now instead of waiting out the idle budget.
|
|
1189
|
+
() => {
|
|
1190
|
+
const other = oldestLoadedTurn() ?? turns()[0]
|
|
1191
|
+
if (other && other.id !== turn.id) {
|
|
1192
|
+
navTo(path, other.id)
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
)
|
|
1159
1196
|
chainBase += chainPages
|
|
1160
1197
|
if (token !== jumpToken || sessionID() !== sessionAtJump) return
|
|
1161
1198
|
if (landed) {
|
|
@@ -1230,13 +1267,14 @@ function Rail(props) {
|
|
|
1230
1267
|
setWarming(false)
|
|
1231
1268
|
}
|
|
1232
1269
|
|
|
1233
|
-
// Prewarm once per session,
|
|
1234
|
-
//
|
|
1270
|
+
// Prewarm once per session, right after the turn list loads (or on hover /
|
|
1271
|
+
// /turns). Every session gets it: the warm cost for a short session is one
|
|
1272
|
+
// cheap host page, and it guarantees the first click lands instantly.
|
|
1235
1273
|
const maybePrewarm = () => {
|
|
1236
1274
|
const sid = sessionID()
|
|
1237
1275
|
if (!sid || warming()) return
|
|
1238
1276
|
if (warmedFor === sid) return
|
|
1239
|
-
if (turns().length <
|
|
1277
|
+
if (turns().length < 1) return
|
|
1240
1278
|
void prewarm()
|
|
1241
1279
|
}
|
|
1242
1280
|
|
|
@@ -1247,7 +1285,11 @@ function Rail(props) {
|
|
|
1247
1285
|
const sid = sessionID()
|
|
1248
1286
|
if (!path || !oldest || !sid) return
|
|
1249
1287
|
// Already warm: the oldest turn is mounted, so the store has the history.
|
|
1250
|
-
if (
|
|
1288
|
+
if (findRow(oldest.id)) {
|
|
1289
|
+
warmedFor = sid
|
|
1290
|
+
return
|
|
1291
|
+
}
|
|
1292
|
+
if (warmedFor === sid) return
|
|
1251
1293
|
const token = ++warmToken
|
|
1252
1294
|
const anchor = viewAnchor()
|
|
1253
1295
|
const sc = scroller()
|
|
@@ -1261,7 +1303,7 @@ function Rail(props) {
|
|
|
1261
1303
|
// the host goes silent, bounce the hash to re-engage it once it is ready.
|
|
1262
1304
|
// If it never engages, bail fast (without marking warm) so clicks fall
|
|
1263
1305
|
// back to a direct jump instead of all showing "预热中 · 稍等一下" for 2min.
|
|
1264
|
-
const deadline = Date.now() +
|
|
1306
|
+
const deadline = Date.now() + 30000
|
|
1265
1307
|
let lastSig = rowSignature()
|
|
1266
1308
|
let lastChange = Date.now()
|
|
1267
1309
|
let kicks = 0
|
|
@@ -1278,7 +1320,7 @@ function Rail(props) {
|
|
|
1278
1320
|
lastSig = sig
|
|
1279
1321
|
lastChange = now
|
|
1280
1322
|
kicks = 0
|
|
1281
|
-
} else if (now - lastChange >
|
|
1323
|
+
} else if (now - lastChange > 3000) {
|
|
1282
1324
|
if (kicks >= 2) break // host never engaged: give up, stay un-warm
|
|
1283
1325
|
// Re-engage: a repeated identical hash is ignored by the router, so
|
|
1284
1326
|
// bounce through a mounted turn first, then back to the oldest.
|
|
@@ -1343,9 +1385,9 @@ function Rail(props) {
|
|
|
1343
1385
|
const sid = sessionID()
|
|
1344
1386
|
const count = turns().length
|
|
1345
1387
|
const hot = warming()
|
|
1346
|
-
if (!sid || count <
|
|
1388
|
+
if (!sid || count < 1 || hot || warmedFor === sid || warmBlockedFor === sid) return
|
|
1347
1389
|
if (loadingActive || loadedFor !== sid) return
|
|
1348
|
-
const timer = setTimeout(() => maybePrewarm(),
|
|
1390
|
+
const timer = setTimeout(() => maybePrewarm(), 400)
|
|
1349
1391
|
onCleanup(() => clearTimeout(timer))
|
|
1350
1392
|
})
|
|
1351
1393
|
|