opencode-session-turns 0.1.11 → 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.
- package/dist/gui-experimental.cjs +3 -17
- package/package.json +1 -1
- package/src/gui.tsx +36 -66
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const
|
|
2
|
-
`).map(
|
|
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
|
|
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
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 =
|
|
31
|
-
const JUMP_IDLE_MS =
|
|
32
|
-
const JUMP_FIRST_IDLE_MS =
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
1018
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
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
|
-
{
|
|
1145
|
-
|
|
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"
|