opencode-session-turns 0.1.13 → 0.1.15
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/README.md +6 -4
- package/dist/gui-experimental.cjs +45 -17
- package/package.json +1 -1
- package/src/gui.tsx +152 -62
package/README.md
CHANGED
|
@@ -22,11 +22,13 @@ click a turn to jump straight to that message.
|
|
|
22
22
|
- 列出当前会话的每一轮用户提问(从会话消息接口分页拉取,`type=user`)。
|
|
23
23
|
- 鼠标移到窗口右缘的细条上自动展开,移开自动收起;长历史跳转期间面板保持展开。
|
|
24
24
|
- 点击任意一轮,**跳转到对应消息**——包括很久以前、尚未渲染的历史轮次。
|
|
25
|
-
-
|
|
26
|
-
|
|
25
|
+
- 深历史需要逐页加载时,面板头部显示进度,目标轮次带徽标并琥珀色脉冲;
|
|
26
|
+
按 `Esc` 或点 × 可随时取消。
|
|
27
27
|
- **历史预热**:宿主自己加载历史每页之间有 2 秒硬冷却,深历史跳转因此要等好几秒。
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
轮次条会在**打开会话时**(轮次列表加载完、≥15 轮)就后台把历史预热完并还原视口,
|
|
29
|
+
悬停面板或 `/turns` 也会触发。预热完成后所有跳转都是瞬时的。
|
|
30
|
+
- **点击与预热的配合**:点到的轮次已预热 → 直接瞬时跳转;还没预热到 → 头部显示
|
|
31
|
+
「预热中 · 稍等一下」,等预热翻到该轮后自动落地(不打断预热)。
|
|
30
32
|
- 定位成功后精准描边闪烁**那一条用户气泡**(跟随气泡自身圆角),不再整行填色。
|
|
31
33
|
- 自动高亮当前视口所在的轮次。
|
|
32
34
|
- 斜杠命令 `/turns`、快捷键 `⌘⌥R`(Windows/Linux 为 `Ctrl+Alt+R`)展开轮次条。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const
|
|
2
|
-
`).map(p=>p.trim()).find(p=>p.length>0);if(
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const s=require("solid-js/web"),H=require("@opencode/plugin/gui"),a=require("solid-js");var qe=s.template("<div class=sr-status>"),tt=s.template('<span class="sr-title sr-progress">'),rt=s.template("<div class=sr-foot>"),nt=s.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),st=s.template("<span class=sr-title>会话轮次"),ot=s.template("<div class=sr-note>"),at=s.template("<span class=sr-badge>"),it=s.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 Be="v19",ue="message-",Je="session-turns:diag",pe="session-turns:open",lt=40,ct=200,dt=50,ut=30,pt=45e3,ft=2e4,gt=8e3,ht=12e3,mt=220,bt=800;let He=0,fe="",ge="",he="";const Oe=l=>{const o=String(l?.text??"").split(`
|
|
2
|
+
`).map(p=>p.trim()).find(p=>p.length>0);if(o)return o;const d=l?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},Ne=l=>String(l).padStart(2,"0"),wt=l=>{const o=new Date(l??0);return`${Ne(o.getHours())}:${Ne(o.getMinutes())}`},le=l=>new Promise(o=>setTimeout(o,l)),me=()=>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 d=getComputedStyle(o);if(/(auto|scroll)/.test(d.overflowY)&&o.scrollHeight>o.clientHeight+1)return o;o=o.parentElement}})(),k=l=>{const o=document.getElementById(`${ue}${l}`);if(o)return o;const d=`[data-message-id="${l.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},ce=l=>{const o=me(),d=[...document.querySelectorAll("[data-message-id]")],p=d[0]?.getAttribute("data-message-id")??"",E=he?` · 目标 ${k(he)?"在":"不在"} DOM`:"",_=p?` · 行ID ${p.slice(0,20)}`:"";let C="(空)";try{C=l.router.path()||"(空)"}catch{}return`轮次 ${He} · 容器 ${o?"OK":"缺失"} · DOM ${d.length} 行${E}${_} · 路由 ${C} · hash ${window.location.hash||"(空)"}${ge?` · ${ge}`:""}${fe?` · ${fe}`:""}`},xt=l=>{const o=l.getBoundingClientRect().top;let d=o;for(const p of l.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>o+4||(d=Math.max(d,_.bottom)))}return d},de=l=>{try{const o=me();if(!o||!l.isConnected)return;const d=l.querySelector('[data-slot="user-message-text"]')??l,p=xt(o),E=d.getBoundingClientRect().top-(p+18);Math.abs(E)>2&&(o.scrollTop+=E),ge=`inset ${Math.round(p)} · delta ${Math.round(E)}`}catch{}},te=l=>{de(l),setTimeout(()=>de(l),450),setTimeout(()=>de(l),1e3)},ze=l=>{let o="";try{o=l.router.path()||""}catch{}return o.startsWith("/")?o:""},vt=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()}},yt=`
|
|
3
3
|
.sr-rail {
|
|
4
4
|
position: fixed;
|
|
5
5
|
top: 50%;
|
|
@@ -60,7 +60,10 @@
|
|
|
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
|
-
/* the scrollable list; the header stays fixed above it
|
|
63
|
+
/* the scrollable list; the header stays fixed above it.
|
|
64
|
+
The collapsed rail must NOT be a scroll container: centering the content
|
|
65
|
+
inside a scroller overflows in both directions, so the browser clamps the
|
|
66
|
+
scroll offset and the list snaps to the top/bottom when the panel opens. */
|
|
64
67
|
.sr-list {
|
|
65
68
|
flex: 1;
|
|
66
69
|
min-height: 0;
|
|
@@ -68,11 +71,15 @@
|
|
|
68
71
|
flex-direction: column;
|
|
69
72
|
justify-content: center;
|
|
70
73
|
gap: 2px;
|
|
71
|
-
overflow
|
|
72
|
-
overflow-x: hidden;
|
|
74
|
+
overflow: hidden;
|
|
73
75
|
scrollbar-width: none;
|
|
74
76
|
}
|
|
75
|
-
.sr-rail[data-open="true"] .sr-list {
|
|
77
|
+
.sr-rail[data-open="true"] .sr-list {
|
|
78
|
+
justify-content: flex-start;
|
|
79
|
+
gap: 1px;
|
|
80
|
+
overflow-y: auto;
|
|
81
|
+
scrollbar-width: thin;
|
|
82
|
+
}
|
|
76
83
|
.sr-list::-webkit-scrollbar { display: none; }
|
|
77
84
|
.sr-rail[data-open="true"] .sr-list::-webkit-scrollbar { display: block; width: 8px; }
|
|
78
85
|
.sr-list::-webkit-scrollbar-track { background: transparent; }
|
|
@@ -94,9 +101,6 @@
|
|
|
94
101
|
color: inherit;
|
|
95
102
|
font: inherit;
|
|
96
103
|
border-radius: 4px;
|
|
97
|
-
/* long sessions: skip layout/paint for rows outside the panel viewport */
|
|
98
|
-
content-visibility: auto;
|
|
99
|
-
contain-intrinsic-size: 0 26px;
|
|
100
104
|
}
|
|
101
105
|
/* expanded: dash flips to a left-side indicator, content reads left-to-right */
|
|
102
106
|
.sr-rail[data-open="true"] .sr-turn {
|
|
@@ -192,6 +196,17 @@
|
|
|
192
196
|
box-shadow: inset 0 0 0 2px rgba(255,170,60,0), inset 0 0 0 rgba(255,170,60,0);
|
|
193
197
|
}
|
|
194
198
|
}
|
|
199
|
+
.sr-badge {
|
|
200
|
+
font-size: 10px;
|
|
201
|
+
line-height: 1.4;
|
|
202
|
+
padding: 0 5px;
|
|
203
|
+
border-radius: 4px;
|
|
204
|
+
color: rgba(255,170,60,.95);
|
|
205
|
+
background: rgba(255,170,60,.14);
|
|
206
|
+
border: 1px solid rgba(255,170,60,.35);
|
|
207
|
+
flex-shrink: 0;
|
|
208
|
+
white-space: nowrap;
|
|
209
|
+
}
|
|
195
210
|
.sr-note {
|
|
196
211
|
display: none;
|
|
197
212
|
font-size: 11px;
|
|
@@ -199,19 +214,32 @@
|
|
|
199
214
|
padding: 2px;
|
|
200
215
|
}
|
|
201
216
|
.sr-rail[data-open="true"] .sr-note { display: block; }
|
|
217
|
+
.sr-foot {
|
|
218
|
+
flex-shrink: 0;
|
|
219
|
+
font-size: 11px;
|
|
220
|
+
line-height: 1.5;
|
|
221
|
+
color: rgba(255,170,60,.92);
|
|
222
|
+
padding: 6px 2px 0;
|
|
223
|
+
margin-top: 6px;
|
|
224
|
+
border-top: 1px solid rgba(127,127,127,.2);
|
|
225
|
+
white-space: pre-wrap;
|
|
226
|
+
max-height: 64px;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
}
|
|
202
229
|
.sr-status {
|
|
203
230
|
position: fixed;
|
|
204
|
-
right:
|
|
205
|
-
|
|
231
|
+
right: 42px;
|
|
232
|
+
top: 50%;
|
|
233
|
+
transform: translateY(-50%);
|
|
206
234
|
z-index: 401;
|
|
207
235
|
font-size: 11px;
|
|
208
|
-
|
|
236
|
+
line-height: 1.5;
|
|
237
|
+
padding: 4px 9px;
|
|
209
238
|
border-radius: 6px;
|
|
210
|
-
background: var(--background-strong, rgba(0,0,0,.
|
|
239
|
+
background: var(--background-strong, rgba(0,0,0,.82));
|
|
211
240
|
color: var(--text-base, #fff);
|
|
212
|
-
|
|
213
|
-
pointer-events: none;
|
|
241
|
+
border: 1px solid rgba(127,127,127,.25);
|
|
214
242
|
white-space: pre-wrap;
|
|
215
|
-
max-width:
|
|
243
|
+
max-width: 260px;
|
|
216
244
|
}
|
|
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;
|
|
245
|
+
`;function kt(l){const o=H.usePlugin(),[d,p]=a.createSignal([]),[E,_]=a.createSignal(""),[C,M]=a.createSignal(!1),[Fe,Ue]=a.createSignal(""),[D,be]=a.createSignal(""),[y,F]=a.createSignal(!1),[we,P]=a.createSignal(0),[xe,j]=a.createSignal(0),[$t,R]=a.createSignal(0),[U,W]=a.createSignal(""),[ve,q]=a.createSignal(!1),S=new Map;let re=0,B=!1,G=!1,V="",J;const h=()=>l.session()?.id,ne=()=>{const e=[...S.values()].sort((r,i)=>r.created-i.created),t=a.untrack(d);t.length===e.length&&t.every((r,i)=>r===e[i])||(p(e),He=e.length)},ye=e=>{const t=S.get(e.id);t&&t.created===e.time&&t.text===e.text||S.set(e.id,{id:e.id,created:e.time,text:e.text})},We=async(e,t,r)=>{const i=r?{sessionID:e,type:"user",limit:t,cursor:r}:{sessionID:e,type:"user",limit:t,order:"desc"};return await o.client.message.list(i)},se=async(e,t)=>{const r=++re;B=!0,t==="full"&&(z="");let i="";try{let n;const c=t==="full"?lt:1,u=t==="full"?ct:dt;let m=t!=="full";for(let w=0;w<c;w++){let b;try{b=await We(e,u,n)}catch(g){i=String(g?.message??g);break}if(r!==re)return;m||(S.clear(),m=!0);let x=0;for(const g of b?.data??[])S.has(g.id)||(ye({id:g.id,time:g.time?.created??0,text:Oe(g)}),x++);ne();const f=b?.cursor?.next;if(!f||x===0)break;n=f}}finally{B=!1,V=e,fe=i,Ue(S.size===0?i||"暂无用户消息":""),G&&(G=!1,ke()),t==="full"&&!i&&e===a.untrack(h)&&S.size>=15&&setTimeout(()=>Q(),800)}},ke=()=>{J||(J=setTimeout(()=>{J=void 0;const e=a.untrack(h);e&&se(e,"light")},300))},Se=e=>{if(!(!e||e!==a.untrack(h))){if(B){G=!0;return}ke()}},Ge=e=>{const t=e?.type;if(t==="message.updated"){const r=e?.data??{},i=r.info;i?.role==="user"&&i?.id&&(ye({id:i.id,time:i.time?.created??0,text:Oe(i)}),ne()),Se(r.sessionID)}else if(t==="message.removed"){const r=e?.data??{};r.messageID&&S.delete(r.messageID)&&ne(),Se(r.sessionID)}else if(t==="location.shutdown"){const r=a.untrack(h);r&&V===r&&se(r,"full")}};a.onMount(()=>{let e=!1,t=0,r;(async()=>{for(;!e;){const c=o.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!e;){const u=await c.next();if(u.done)break;t=0,Ge(u.value)}}catch{}try{await c.return?.()}catch{}if(e)break;t++,await le(Math.min(2e3*t,15e3))}})(),a.onCleanup(()=>{e=!0,clearTimeout(r),J&&clearTimeout(J)})}),a.createEffect(()=>{const e=h();e&&a.untrack(()=>{V!==e&&(re++,B=!1,G=!1,S.clear(),p([]),_(""),ae="",se(e,"full"))})});let Y;const I=()=>(Y?.isConnected||(Y=me()),Y),Ve=()=>{const e=I();if(!e)return;const t=e.querySelectorAll("[data-message-id]");if(t.length===0)return;const r=e.getBoundingClientRect().top+40;for(const i of t)if(i.getBoundingClientRect().bottom>=r){const n=i.getAttribute("data-message-id");n&&_(n);break}};let oe=!1;const $e=()=>{oe||(oe=!0,requestAnimationFrame(()=>{oe=!1,Ve()}))};a.onMount(()=>{const e=()=>$e();document.addEventListener("scroll",e,!0);const t=setInterval($e,1500);a.onCleanup(()=>{document.removeEventListener("scroll",e,!0),clearInterval(t)})});let Ee=0;const K=(e,t=2600)=>{const r=++Ee;be(e),setTimeout(()=>{r===Ee&&be("")},t)};a.onMount(()=>{const e=r=>{if(r.key==="Escape"){if(a.untrack(y)){Me(),r.stopPropagation();return}a.untrack(C)&&(M(!1),r.stopPropagation())}},t=r=>{if(!a.untrack(C))return;(r.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||M(!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=()=>K(`${Be} · ${ce(o)}`),t=()=>{M(r=>!r),Q()};window.addEventListener(Je,e),window.addEventListener(pe,t),a.onCleanup(()=>{window.removeEventListener(Je,e),window.removeEventListener(pe,t)})});let ae="",v=0,_e=0,Ce="";const Z=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 d())if(k(e.id))return e},O=(e,t)=>{vt(`${e}#${ue}${t}`),ae=t},Te=()=>{const t=I()?.querySelectorAll("[data-message-id]")??[];if(t.length===0)return"0";const r=t[0].getAttribute("data-message-id")??"",i=t[t.length-1].getAttribute("data-message-id")??"";return`${t.length}:${r}:${i}`},Ae=(e,t,r,i)=>new Promise(n=>{let c=!1,u,m,w;const b=Date.now();let f=Te(),g=b,T=0;const $=ee=>{if(!c){c=!0;try{u?.disconnect()}catch{}clearInterval(m),clearTimeout(w),n(ee??null)}},A=ee=>{if(t!==v)return $(null);const Pe=ee?k(e):document.getElementById(`${ue}${e}`);if(Pe)return $(Pe);if(!ee)return;const je=Te(),Re=Date.now();if(je!==f){f=je,g=Re,T++,i?.(T);return}const et=T>0?gt:ht;Re-g>et&&$(null)},De=k(e);if(De)return $(De);u=new MutationObserver(()=>A(!1)),u.observe(document.documentElement,{childList:!0,subtree:!0}),m=setInterval(()=>A(!0),120),w=setTimeout(()=>$(k(e)),Math.max(400,r))}),Me=()=>{v++,F(!1),P(0),j(0),R(0),W(""),q(!1),K("已取消跳转")},Ke=async e=>{if(y()&&U()===e.id)return;const t=ze(o),r=I(),i=k(e.id);if(i&&r){L()&&(X(),t&&O(t,e.id)),te(i),Z(i);return}const n=Date.now();if(e.id===Ce&&n-_e<bt){const x=k(e.id);if(x&&r){te(x),Z(x);return}}if(_e=n,Ce=e.id,he=e.id,!t){K(`无法读取当前路由,无法定位 · ${ce(o)}`);return}const c=++v,u=h();F(!0),W(e.id),P(0),j(0),R(0),q(L()),M(!0);const m=Date.now(),w=setInterval(()=>{j(Math.round((Date.now()-m)/1e3))},500);let b=0;try{if(L()){const f=await Ae(e.id,c,12e4,g=>P(g));if(c!==v||h()!==u)return;if(f){X(),O(t,e.id),te(f),Z(f);return}if(L()&&X(),q(!1),c!==v||h()!==u)return}const x=Date.now()+pt;for(let f=0;f<ut&&Date.now()<x;f++){if(c!==v||h()!==u)return;if(R(f+1),ae===e.id){const A=Ye()??d()[0];A&&(O(t,A.id),await le(mt))}if(c!==v||h()!==u)return;O(t,e.id);let g=0;const T=Math.min(ft,x-Date.now()),$=await Ae(e.id,c,T,A=>{g=A,P(b+A)});if(b+=g,c!==v||h()!==u)return;if($){te($),Z($);return}}c===v&&K(`未能定位 · ${ce(o)}`)}finally{clearInterval(w),c===v&&(F(!1),P(0),j(0),R(0),W(""),q(!1))}},[L,ie]=a.createSignal(!1);let N=0,z="";const Ze=()=>{const e=I();if(!e)return;const t=e.getBoundingClientRect();for(const r of e.querySelectorAll("[data-message-id]")){const i=r.getBoundingClientRect();if(i.bottom<=t.top+1)continue;const n=r.getAttribute("data-message-id");if(n)return{el:r,id:n,offset:i.top-t.top}}},Ie=e=>{if(!e)return;const t=I();if(!t)return;const r=e.el?.isConnected?e.el:k(e.id);if(!r)return;const i=t.getBoundingClientRect(),n=r.getBoundingClientRect().top-i.top-e.offset;Math.abs(n)>2&&(t.scrollTop+=n)},X=()=>{N++,ie(!1)},Q=()=>{const e=h();!e||L()||z!==e&&(d().length<15||Xe())},Xe=async()=>{if(L())return;const e=ze(o),t=d()[0],r=h();if(!e||!t||!r||z===r&&k(t.id))return;const i=++N,n=Ze(),c=I(),u=c?c.scrollTop+c.clientHeight>=c.scrollHeight-32:!0;ie(!0);try{O(e,t.id);const m=Date.now()+12e4,w=setInterval(()=>Ie(n),150);try{for(;Date.now()<m&&i===N&&!k(t.id);)await le(250)}finally{clearInterval(w)}if(i!==N)return;if(Ie(n),u){const b=I();b&&(b.scrollTop=b.scrollHeight)}z=r}finally{i===N&&ie(!1)}};let Le=a.untrack(h);a.createEffect(()=>{const e=h();e!==Le&&(Le=e,X(),v++,F(!1),P(0),j(0),R(0),W(""),q(!1))}),a.createEffect(()=>{const e=h(),t=d().length,r=L();if(!e||t<15||r||z===e||B||V!==e)return;const i=setTimeout(()=>Q(),800);a.onCleanup(()=>clearTimeout(i))}),a.createEffect(()=>{y()&&U()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Qe=[s.createComponent(a.Show,{get when(){return D()},get children(){var e=qe();return s.insert(e,D),e}}),(()=>{var e=nt(),t=e.firstChild,r=t.firstChild,i=t.nextSibling;return e.addEventListener("mouseleave",()=>{y()||M(!1)}),e.addEventListener("mouseenter",()=>{M(!0),Q()}),s.insert(t,s.createComponent(a.Show,{get when(){return y()},get fallback(){return(()=>{var n=st();return n.firstChild,s.insert(n,(()=>{var c=s.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),n})()},get children(){var n=tt();return s.insert(n,()=>ve()?"预热中 · 稍等一下":"加载历史中",null),s.insert(n,(()=>{var c=s.memo(()=>we()>0);return()=>c()?` · ${we()} 页`:""})(),null),s.insert(n,(()=>{var c=s.memo(()=>xe()>0);return()=>c()?` · ${xe()}s`:""})(),null),n}}),r),r.$$click=()=>{y()?Me():M(!1)},s.insert(i,s.createComponent(a.Show,{get when(){return d().length>0},get fallback(){return(()=>{var n=ot();return s.insert(n,()=>Fe()||`… ${Be}`),n})()},get children(){return s.createComponent(a.For,{get each(){return d()},children:(n,c)=>(()=>{var u=it(),m=u.firstChild,w=m.firstChild;w.firstChild;var b=w.nextSibling,x=m.nextSibling;return u.$$click=()=>{Ke(n)},s.insert(w,()=>c()+1,null),s.insert(b,()=>wt(n.created)),s.insert(m,()=>n.text,null),s.insert(u,s.createComponent(a.Show,{get when(){return s.memo(()=>!!y())()&&U()===n.id},get children(){var f=at();return s.insert(f,()=>ve()?"稍等":"加载中"),f}}),x),s.effect(f=>{var g=E()===n.id?"true":"false",T=y()&&U()===n.id?"true":"false";return g!==f.e&&s.setAttribute(u,"data-active",f.e=g),T!==f.t&&s.setAttribute(u,"data-busy",f.t=T),f},{e:void 0,t:void 0}),u})()})}})),s.insert(e,s.createComponent(a.Show,{get when(){return s.memo(()=>!!C())()&&D()},get children(){var n=rt();return s.insert(n,D),n}}),null),s.effect(n=>{var c=C()?"true":"false",u=y()?"true":"false",m=y()?"取消跳转 (Esc)":"关闭 (Esc)";return c!==n.e&&s.setAttribute(e,"data-open",n.e=c),u!==n.t&&s.setAttribute(e,"data-jumping",n.t=u),m!==n.a&&s.setAttribute(r,"title",n.a=m),n},{e:void 0,t:void 0,a:void 0}),e})(),s.createComponent(a.Show,{get when(){return s.memo(()=>!C())()&&D()},get children(){var e=qe();return s.insert(e,D),e}})];return s.createComponent(s.Portal,{get mount(){return document.body},children:Qe})}const St=H.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(l){l.add(H.Style,yt),l.add(H.Slot,{at:"window.bottom",render:()=>s.createComponent(a.Show,{get when(){return l.sessions.current()},get children(){return s.createComponent(kt,{session:()=>l.sessions.current()})}})}),l.add(H.Command,()=>({id:"jump",title:l.t("command.jump"),description:l.t("command.jump.description"),group:l.t("command.jump"),section:"navigation",bind:"mod+alt+r",slash:{name:"turns"},run:()=>{window.dispatchEvent(new Event(pe))}}))}});s.delegateEvents(["click"]);exports.default=St;
|
package/package.json
CHANGED
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 = "v19"
|
|
10
10
|
const HASH_PREFIX = "message-"
|
|
11
11
|
const DIAG_EVENT = "session-turns:diag"
|
|
12
12
|
const OPEN_EVENT = "session-turns:open"
|
|
@@ -253,7 +253,10 @@ const CSS = `
|
|
|
253
253
|
flex-shrink: 0;
|
|
254
254
|
}
|
|
255
255
|
.sr-close:hover { opacity: 1; background: var(--surface-base-hover, rgba(127,127,127,.14)); }
|
|
256
|
-
/* the scrollable list; the header stays fixed above it
|
|
256
|
+
/* the scrollable list; the header stays fixed above it.
|
|
257
|
+
The collapsed rail must NOT be a scroll container: centering the content
|
|
258
|
+
inside a scroller overflows in both directions, so the browser clamps the
|
|
259
|
+
scroll offset and the list snaps to the top/bottom when the panel opens. */
|
|
257
260
|
.sr-list {
|
|
258
261
|
flex: 1;
|
|
259
262
|
min-height: 0;
|
|
@@ -261,11 +264,15 @@ const CSS = `
|
|
|
261
264
|
flex-direction: column;
|
|
262
265
|
justify-content: center;
|
|
263
266
|
gap: 2px;
|
|
264
|
-
overflow
|
|
265
|
-
overflow-x: hidden;
|
|
267
|
+
overflow: hidden;
|
|
266
268
|
scrollbar-width: none;
|
|
267
269
|
}
|
|
268
|
-
.sr-rail[data-open="true"] .sr-list {
|
|
270
|
+
.sr-rail[data-open="true"] .sr-list {
|
|
271
|
+
justify-content: flex-start;
|
|
272
|
+
gap: 1px;
|
|
273
|
+
overflow-y: auto;
|
|
274
|
+
scrollbar-width: thin;
|
|
275
|
+
}
|
|
269
276
|
.sr-list::-webkit-scrollbar { display: none; }
|
|
270
277
|
.sr-rail[data-open="true"] .sr-list::-webkit-scrollbar { display: block; width: 8px; }
|
|
271
278
|
.sr-list::-webkit-scrollbar-track { background: transparent; }
|
|
@@ -287,9 +294,6 @@ const CSS = `
|
|
|
287
294
|
color: inherit;
|
|
288
295
|
font: inherit;
|
|
289
296
|
border-radius: 4px;
|
|
290
|
-
/* long sessions: skip layout/paint for rows outside the panel viewport */
|
|
291
|
-
content-visibility: auto;
|
|
292
|
-
contain-intrinsic-size: 0 26px;
|
|
293
297
|
}
|
|
294
298
|
/* expanded: dash flips to a left-side indicator, content reads left-to-right */
|
|
295
299
|
.sr-rail[data-open="true"] .sr-turn {
|
|
@@ -385,6 +389,17 @@ const CSS = `
|
|
|
385
389
|
box-shadow: inset 0 0 0 2px rgba(255,170,60,0), inset 0 0 0 rgba(255,170,60,0);
|
|
386
390
|
}
|
|
387
391
|
}
|
|
392
|
+
.sr-badge {
|
|
393
|
+
font-size: 10px;
|
|
394
|
+
line-height: 1.4;
|
|
395
|
+
padding: 0 5px;
|
|
396
|
+
border-radius: 4px;
|
|
397
|
+
color: rgba(255,170,60,.95);
|
|
398
|
+
background: rgba(255,170,60,.14);
|
|
399
|
+
border: 1px solid rgba(255,170,60,.35);
|
|
400
|
+
flex-shrink: 0;
|
|
401
|
+
white-space: nowrap;
|
|
402
|
+
}
|
|
388
403
|
.sr-note {
|
|
389
404
|
display: none;
|
|
390
405
|
font-size: 11px;
|
|
@@ -392,20 +407,33 @@ const CSS = `
|
|
|
392
407
|
padding: 2px;
|
|
393
408
|
}
|
|
394
409
|
.sr-rail[data-open="true"] .sr-note { display: block; }
|
|
410
|
+
.sr-foot {
|
|
411
|
+
flex-shrink: 0;
|
|
412
|
+
font-size: 11px;
|
|
413
|
+
line-height: 1.5;
|
|
414
|
+
color: rgba(255,170,60,.92);
|
|
415
|
+
padding: 6px 2px 0;
|
|
416
|
+
margin-top: 6px;
|
|
417
|
+
border-top: 1px solid rgba(127,127,127,.2);
|
|
418
|
+
white-space: pre-wrap;
|
|
419
|
+
max-height: 64px;
|
|
420
|
+
overflow: hidden;
|
|
421
|
+
}
|
|
395
422
|
.sr-status {
|
|
396
423
|
position: fixed;
|
|
397
|
-
right:
|
|
398
|
-
|
|
424
|
+
right: 42px;
|
|
425
|
+
top: 50%;
|
|
426
|
+
transform: translateY(-50%);
|
|
399
427
|
z-index: 401;
|
|
400
428
|
font-size: 11px;
|
|
401
|
-
|
|
429
|
+
line-height: 1.5;
|
|
430
|
+
padding: 4px 9px;
|
|
402
431
|
border-radius: 6px;
|
|
403
|
-
background: var(--background-strong, rgba(0,0,0,.
|
|
432
|
+
background: var(--background-strong, rgba(0,0,0,.82));
|
|
404
433
|
color: var(--text-base, #fff);
|
|
405
|
-
|
|
406
|
-
pointer-events: none;
|
|
434
|
+
border: 1px solid rgba(127,127,127,.25);
|
|
407
435
|
white-space: pre-wrap;
|
|
408
|
-
max-width:
|
|
436
|
+
max-width: 260px;
|
|
409
437
|
}
|
|
410
438
|
`
|
|
411
439
|
|
|
@@ -427,6 +455,9 @@ function Rail(props) {
|
|
|
427
455
|
const [jumpElapsed, setJumpElapsed] = createSignal(0)
|
|
428
456
|
const [jumpChain, setJumpChain] = createSignal(0)
|
|
429
457
|
const [busyID, setBusyID] = createSignal("")
|
|
458
|
+
// True while a click is waiting for the background prewarm to page its target
|
|
459
|
+
// in (header reads "预热中 · 稍等一下" instead of "加载历史中").
|
|
460
|
+
const [waitWarm, setWaitWarm] = createSignal(false)
|
|
430
461
|
|
|
431
462
|
// ---- turn cache ---------------------------------------------------------
|
|
432
463
|
// Object identity is kept stable across refreshes so Solid's <For> reuses
|
|
@@ -469,6 +500,9 @@ function Rail(props) {
|
|
|
469
500
|
const load = async (id, mode) => {
|
|
470
501
|
const token = ++loadToken
|
|
471
502
|
loadingActive = true
|
|
503
|
+
// A full (re)load means the host store may be cold again (session open or
|
|
504
|
+
// server rebuild): invalidate the prewarm marker so it runs once more.
|
|
505
|
+
if (mode === "full") warmedFor = ""
|
|
472
506
|
let error = ""
|
|
473
507
|
try {
|
|
474
508
|
let cursor
|
|
@@ -515,6 +549,11 @@ function Rail(props) {
|
|
|
515
549
|
pendingBump = false
|
|
516
550
|
scheduleLight()
|
|
517
551
|
}
|
|
552
|
+
// Prewarm long sessions as soon as their full turn list has loaded, so
|
|
553
|
+
// the history is warm before the first click (not only on rail hover).
|
|
554
|
+
if (mode === "full" && !error && id === untrack(sessionID) && cache.size >= 15) {
|
|
555
|
+
setTimeout(() => maybePrewarm(), 800)
|
|
556
|
+
}
|
|
518
557
|
}
|
|
519
558
|
}
|
|
520
559
|
|
|
@@ -842,39 +881,40 @@ function Rail(props) {
|
|
|
842
881
|
setJumpElapsed(0)
|
|
843
882
|
setJumpChain(0)
|
|
844
883
|
setBusyID("")
|
|
884
|
+
setWaitWarm(false)
|
|
845
885
|
flash("已取消跳转")
|
|
846
886
|
}
|
|
847
887
|
|
|
848
888
|
const jump = async (turn) => {
|
|
849
|
-
// A jump takes priority over a background prewarm.
|
|
850
|
-
if (warming()) cancelWarm()
|
|
851
|
-
|
|
852
889
|
// Clicking the same turn again while its jump is running must NOT restart
|
|
853
890
|
// the loop: every restart cancels the host's in-flight page load, which is
|
|
854
|
-
// what makes a deep jump advance only one page per click.
|
|
855
|
-
// progress
|
|
856
|
-
if (jumping() && busyID() === turn.id)
|
|
857
|
-
|
|
891
|
+
// what makes a deep jump advance only one page per click. The header
|
|
892
|
+
// already shows the progress, so just leave it running.
|
|
893
|
+
if (jumping() && busyID() === turn.id) return
|
|
894
|
+
|
|
895
|
+
const path = routePath(ctx)
|
|
896
|
+
const sc = scroller()
|
|
897
|
+
const mounted = findRow(turn.id)
|
|
898
|
+
|
|
899
|
+
// Fast path: the row is already mounted (e.g. the prewarm reached it), so
|
|
900
|
+
// the jump is instant. If a prewarm is still running, re-target the host's
|
|
901
|
+
// pending oldest-seek to this turn first - otherwise the host would scroll
|
|
902
|
+
// to the oldest message when that seek completes and yank the viewport.
|
|
903
|
+
if (mounted && sc) {
|
|
904
|
+
if (warming()) {
|
|
905
|
+
cancelWarm()
|
|
906
|
+
if (path) navTo(path, turn.id)
|
|
907
|
+
}
|
|
908
|
+
alignAfterLand(mounted)
|
|
909
|
+
pulse(mounted)
|
|
858
910
|
return
|
|
859
911
|
}
|
|
860
912
|
|
|
861
|
-
const token = ++jumpToken
|
|
862
|
-
// A new click always owns the progress UI. This also clears a previous
|
|
863
|
-
// jump's state when the new target takes the mounted fast path, where the
|
|
864
|
-
// superseded loop's finally no longer touches these signals.
|
|
865
|
-
setJumping(false)
|
|
866
|
-
setJumpPages(0)
|
|
867
|
-
setJumpElapsed(0)
|
|
868
|
-
setJumpChain(0)
|
|
869
|
-
setBusyID("")
|
|
870
|
-
lastJumpID = turn.id
|
|
871
913
|
const now = Date.now()
|
|
872
|
-
|
|
873
914
|
// Same target clicked again shortly after: treat as idempotent re-scroll
|
|
874
915
|
// instead of restarting the paging loop.
|
|
875
916
|
if (turn.id === lastJumpTurnID && now - lastJumpAt < SAME_TARGET_WINDOW_MS) {
|
|
876
917
|
const row = findRow(turn.id)
|
|
877
|
-
const sc = scroller()
|
|
878
918
|
if (row && sc) {
|
|
879
919
|
alignAfterLand(row)
|
|
880
920
|
pulse(row)
|
|
@@ -883,46 +923,23 @@ function Rail(props) {
|
|
|
883
923
|
}
|
|
884
924
|
lastJumpAt = now
|
|
885
925
|
lastJumpTurnID = turn.id
|
|
926
|
+
lastJumpID = turn.id
|
|
886
927
|
|
|
887
|
-
const sc = scroller()
|
|
888
|
-
const row = findRow(turn.id)
|
|
889
|
-
|
|
890
|
-
// Fast path: the row is mounted, scroll locally. No toast: the bubble
|
|
891
|
-
// ring is the feedback.
|
|
892
|
-
if (row && sc) {
|
|
893
|
-
alignAfterLand(row)
|
|
894
|
-
pulse(row)
|
|
895
|
-
return
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
const path = routePath(ctx)
|
|
899
928
|
if (!path) {
|
|
900
929
|
flash(`无法读取当前路由,无法定位 · ${diagnostics(ctx)}`)
|
|
901
930
|
return
|
|
902
931
|
}
|
|
903
932
|
|
|
904
|
-
|
|
905
|
-
//
|
|
906
|
-
// The host's own seek machine does the heavy lifting: hash.changed ->
|
|
907
|
-
// applying -> pending(target) -> target.synced -> seek, and it loads the
|
|
908
|
-
// missing history asynchronously (up to 12 internal retries). Every extra
|
|
909
|
-
// hash change we send RESETS that pending state and aborts the history
|
|
910
|
-
// fetch in flight, which is why hammering the hash made a deep jump
|
|
911
|
-
// advance a single page at a time.
|
|
912
|
-
//
|
|
913
|
-
// So: set the hash once, then be patient. Only when the host has been
|
|
914
|
-
// completely idle for JUMP_IDLE_MS do we bounce the hash through a
|
|
915
|
-
// mounted turn to kick off a fresh chain.
|
|
933
|
+
const token = ++jumpToken
|
|
916
934
|
const sessionAtJump = sessionID()
|
|
917
935
|
setJumping(true)
|
|
918
936
|
setBusyID(turn.id)
|
|
919
937
|
setJumpPages(0)
|
|
920
938
|
setJumpElapsed(0)
|
|
939
|
+
setJumpChain(0)
|
|
940
|
+
setWaitWarm(warming())
|
|
921
941
|
// Pin the panel open even if the pointer has left it meanwhile.
|
|
922
942
|
setOpen(true)
|
|
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)
|
|
926
943
|
const startedAt = Date.now()
|
|
927
944
|
const elapsedTimer = setInterval(() => {
|
|
928
945
|
setJumpElapsed(Math.round((Date.now() - startedAt) / 1000))
|
|
@@ -931,6 +948,34 @@ function Rail(props) {
|
|
|
931
948
|
// the per-chain counter, the headline must not go backwards).
|
|
932
949
|
let chainBase = 0
|
|
933
950
|
try {
|
|
951
|
+
// A prewarm is already paging the history (it seeks the oldest turn, so
|
|
952
|
+
// it passes through this one on the way). Don't fight it with a competing
|
|
953
|
+
// seek: just wait for the row to mount, then land. The header reads
|
|
954
|
+
// "预热中 · 稍等一下" while we wait.
|
|
955
|
+
if (warming()) {
|
|
956
|
+
const landed = await waitForRow(turn.id, token, 120000, (pages) => setJumpPages(pages))
|
|
957
|
+
if (token !== jumpToken || sessionID() !== sessionAtJump) return
|
|
958
|
+
if (landed) {
|
|
959
|
+
cancelWarm()
|
|
960
|
+
navTo(path, turn.id)
|
|
961
|
+
alignAfterLand(landed)
|
|
962
|
+
pulse(landed)
|
|
963
|
+
return
|
|
964
|
+
}
|
|
965
|
+
// The prewarm ran out without mounting the row: fall back to driving
|
|
966
|
+
// the host's seek ourselves below.
|
|
967
|
+
if (warming()) cancelWarm()
|
|
968
|
+
setWaitWarm(false)
|
|
969
|
+
if (token !== jumpToken || sessionID() !== sessionAtJump) return
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Deep history, no prewarm. The host's own seek machine does the heavy
|
|
973
|
+
// lifting: hash.changed -> applying -> pending(target) -> target.synced
|
|
974
|
+
// -> seek, loading the missing history asynchronously. Every extra hash
|
|
975
|
+
// change RESETS that pending state and aborts the fetch in flight, which
|
|
976
|
+
// is why hammering the hash made a deep jump advance one page per click.
|
|
977
|
+
// So: set the hash once, then be patient; only bounce it through a
|
|
978
|
+
// mounted turn when the host has been idle for JUMP_IDLE_MS.
|
|
934
979
|
const deadline = Date.now() + JUMP_TOTAL_MS
|
|
935
980
|
for (let chain = 0; chain < JUMP_ROUNDS && Date.now() < deadline; chain++) {
|
|
936
981
|
// Abandon the loop if the user switched sessions mid-jump: toggling the
|
|
@@ -981,6 +1026,7 @@ function Rail(props) {
|
|
|
981
1026
|
setJumpElapsed(0)
|
|
982
1027
|
setJumpChain(0)
|
|
983
1028
|
setBusyID("")
|
|
1029
|
+
setWaitWarm(false)
|
|
984
1030
|
}
|
|
985
1031
|
}
|
|
986
1032
|
}
|
|
@@ -1078,6 +1124,37 @@ function Rail(props) {
|
|
|
1078
1124
|
}
|
|
1079
1125
|
}
|
|
1080
1126
|
|
|
1127
|
+
// Session switch: abort any jump/prewarm in flight - they target the old
|
|
1128
|
+
// route, and toggling its hash would navigate back to the previous session.
|
|
1129
|
+
let guardSid = untrack(sessionID)
|
|
1130
|
+
createEffect(() => {
|
|
1131
|
+
const sid = sessionID()
|
|
1132
|
+
if (sid === guardSid) return
|
|
1133
|
+
guardSid = sid
|
|
1134
|
+
cancelWarm()
|
|
1135
|
+
jumpToken++
|
|
1136
|
+
setJumping(false)
|
|
1137
|
+
setJumpPages(0)
|
|
1138
|
+
setJumpElapsed(0)
|
|
1139
|
+
setJumpChain(0)
|
|
1140
|
+
setBusyID("")
|
|
1141
|
+
setWaitWarm(false)
|
|
1142
|
+
})
|
|
1143
|
+
|
|
1144
|
+
// Resume the prewarm after a jump interrupted it, so the remaining older
|
|
1145
|
+
// history still gets warmed for the next click. The initial start happens in
|
|
1146
|
+
// load()'s finally (once the full turn list is in); this only fires when the
|
|
1147
|
+
// warming signal flips back to false while the session is not fully warm.
|
|
1148
|
+
createEffect(() => {
|
|
1149
|
+
const sid = sessionID()
|
|
1150
|
+
const count = turns().length
|
|
1151
|
+
const hot = warming()
|
|
1152
|
+
if (!sid || count < 15 || hot || warmedFor === sid) return
|
|
1153
|
+
if (loadingActive || loadedFor !== sid) return
|
|
1154
|
+
const timer = setTimeout(() => maybePrewarm(), 800)
|
|
1155
|
+
onCleanup(() => clearTimeout(timer))
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1081
1158
|
// Keep the busy row visible in the panel list while the jump pages through
|
|
1082
1159
|
// history, so the "which turn am I waiting for" answer stays on screen.
|
|
1083
1160
|
createEffect(() => {
|
|
@@ -1120,7 +1197,8 @@ function Rail(props) {
|
|
|
1120
1197
|
}
|
|
1121
1198
|
>
|
|
1122
1199
|
<span class="sr-title sr-progress">
|
|
1123
|
-
|
|
1200
|
+
{waitWarm() ? "预热中 · 稍等一下" : "加载历史中"}
|
|
1201
|
+
{jumpPages() > 0 ? ` · ${jumpPages()} 页` : ""}
|
|
1124
1202
|
{jumpElapsed() > 0 ? ` · ${jumpElapsed()}s` : ""}
|
|
1125
1203
|
</span>
|
|
1126
1204
|
</Show>
|
|
@@ -1152,13 +1230,25 @@ function Rail(props) {
|
|
|
1152
1230
|
<span class="sr-time">{formatTime(turn.created)}</span>
|
|
1153
1231
|
{turn.text}
|
|
1154
1232
|
</span>
|
|
1233
|
+
<Show when={jumping() && busyID() === turn.id}>
|
|
1234
|
+
<span class="sr-badge">{waitWarm() ? "稍等" : "加载中"}</span>
|
|
1235
|
+
</Show>
|
|
1155
1236
|
<span class="sr-dash" />
|
|
1156
1237
|
</button>
|
|
1157
1238
|
)}
|
|
1158
1239
|
</For>
|
|
1159
1240
|
</Show>
|
|
1160
1241
|
</div>
|
|
1242
|
+
{/* Messages that need attention (errors, cancel confirmations) live in
|
|
1243
|
+
the panel footer while it is open, and next to the collapsed rail
|
|
1244
|
+
when it is closed - the old bottom-right toast was easy to miss. */}
|
|
1245
|
+
<Show when={open() && status()}>
|
|
1246
|
+
<div class="sr-foot">{status()}</div>
|
|
1247
|
+
</Show>
|
|
1161
1248
|
</div>
|
|
1249
|
+
<Show when={!open() && status()}>
|
|
1250
|
+
<div class="sr-status">{status()}</div>
|
|
1251
|
+
</Show>
|
|
1162
1252
|
</>
|
|
1163
1253
|
)
|
|
1164
1254
|
|