opencode-session-turns 0.1.12 → 0.1.14
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 +2 -2
- package/dist/gui-experimental.cjs +45 -31
- package/package.json +1 -1
- package/src/gui.tsx +92 -86
package/README.md
CHANGED
|
@@ -22,8 +22,8 @@ click a turn to jump straight to that message.
|
|
|
22
22
|
- 列出当前会话的每一轮用户提问(从会话消息接口分页拉取,`type=user`)。
|
|
23
23
|
- 鼠标移到窗口右缘的细条上自动展开,移开自动收起;长历史跳转期间面板保持展开。
|
|
24
24
|
- 点击任意一轮,**跳转到对应消息**——包括很久以前、尚未渲染的历史轮次。
|
|
25
|
-
-
|
|
26
|
-
|
|
25
|
+
- 深历史需要逐页加载时,面板头部显示「加载历史中 · N 页 · Xs」,目标轮次带「加载中」
|
|
26
|
+
徽标并琥珀色脉冲;按 `Esc` 或点 × 可随时取消。
|
|
27
27
|
- **历史预热**:宿主自己加载历史每页之间有 2 秒硬冷却,深历史跳转因此要等好几秒。
|
|
28
28
|
轮次条会在你打开面板时(悬停或 `/turns`)后台把历史预热完并还原视口,
|
|
29
29
|
之后所有跳转都是瞬时的;也可以点面板上的「预热」手动触发。
|
|
@@ -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 o=require("solid-js/web"),q=require("@opencode/plugin/gui"),l=require("solid-js");var Le=o.template("<div class=sr-status>"),Xe=o.template('<span class="sr-title sr-progress">加载历史中'),Qe=o.template("<div class=sr-foot>"),et=o.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),tt=o.template("<span class=sr-title>会话轮次"),rt=o.template("<div class=sr-note>"),nt=o.template("<span class=sr-badge>加载中"),st=o.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 De="v16",le="message-",Pe="session-turns:diag",ce="session-turns:open",ot=40,at=200,it=50,lt=30,ct=45e3,dt=2e4,ut=8e3,pt=12e3,ft=220,gt=800;let Be=0,de="",ue="",pe="";const je=a=>{const s=String(a?.text??"").split(`
|
|
2
|
+
`).map(p=>p.trim()).find(p=>p.length>0);if(s)return s;const d=a?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},Re=a=>String(a).padStart(2,"0"),ht=a=>{const s=new Date(a??0);return`${Re(s.getHours())}:${Re(s.getMinutes())}`},se=a=>new Promise(s=>setTimeout(s,a)),fe=()=>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}})(),y=a=>{const s=document.getElementById(`${le}${a}`);if(s)return s;const d=`[data-message-id="${a.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},oe=a=>{const s=fe(),d=[...document.querySelectorAll("[data-message-id]")],p=d[0]?.getAttribute("data-message-id")??"",S=pe?` · 目标 ${y(pe)?"在":"不在"} DOM`:"",$=p?` · 行ID ${p.slice(0,20)}`:"";let E="(空)";try{E=a.router.path()||"(空)"}catch{}return`轮次 ${Be} · 容器 ${s?"OK":"缺失"} · DOM ${d.length} 行${S}${$} · 路由 ${E} · hash ${window.location.hash||"(空)"}${ue?` · ${ue}`:""}${de?` · ${de}`:""}`},mt=a=>{const s=a.getBoundingClientRect().top;let d=s;for(const p of a.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},ae=a=>{try{const s=fe();if(!s||!a.isConnected)return;const d=a.querySelector('[data-slot="user-message-text"]')??a,p=mt(s),S=d.getBoundingClientRect().top-(p+18);Math.abs(S)>2&&(s.scrollTop+=S),ue=`inset ${Math.round(p)} · delta ${Math.round(S)}`}catch{}},ie=a=>{ae(a),setTimeout(()=>ae(a),450),setTimeout(()=>ae(a),1e3)},qe=a=>{let s="";try{s=a.router.path()||""}catch{}return s.startsWith("/")?s:""},bt=a=>{const s=document.createElement("a");s.setAttribute("href",a),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()}},wt=`
|
|
3
3
|
.sr-rail {
|
|
4
4
|
position: fixed;
|
|
5
5
|
top: 50%;
|
|
@@ -60,21 +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
|
-
.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
/* 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. */
|
|
78
67
|
.sr-list {
|
|
79
68
|
flex: 1;
|
|
80
69
|
min-height: 0;
|
|
@@ -82,11 +71,15 @@
|
|
|
82
71
|
flex-direction: column;
|
|
83
72
|
justify-content: center;
|
|
84
73
|
gap: 2px;
|
|
85
|
-
overflow
|
|
86
|
-
overflow-x: hidden;
|
|
74
|
+
overflow: hidden;
|
|
87
75
|
scrollbar-width: none;
|
|
88
76
|
}
|
|
89
|
-
.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
|
+
}
|
|
90
83
|
.sr-list::-webkit-scrollbar { display: none; }
|
|
91
84
|
.sr-rail[data-open="true"] .sr-list::-webkit-scrollbar { display: block; width: 8px; }
|
|
92
85
|
.sr-list::-webkit-scrollbar-track { background: transparent; }
|
|
@@ -108,9 +101,6 @@
|
|
|
108
101
|
color: inherit;
|
|
109
102
|
font: inherit;
|
|
110
103
|
border-radius: 4px;
|
|
111
|
-
/* long sessions: skip layout/paint for rows outside the panel viewport */
|
|
112
|
-
content-visibility: auto;
|
|
113
|
-
contain-intrinsic-size: 0 26px;
|
|
114
104
|
}
|
|
115
105
|
/* expanded: dash flips to a left-side indicator, content reads left-to-right */
|
|
116
106
|
.sr-rail[data-open="true"] .sr-turn {
|
|
@@ -206,6 +196,17 @@
|
|
|
206
196
|
box-shadow: inset 0 0 0 2px rgba(255,170,60,0), inset 0 0 0 rgba(255,170,60,0);
|
|
207
197
|
}
|
|
208
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
|
+
}
|
|
209
210
|
.sr-note {
|
|
210
211
|
display: none;
|
|
211
212
|
font-size: 11px;
|
|
@@ -213,19 +214,32 @@
|
|
|
213
214
|
padding: 2px;
|
|
214
215
|
}
|
|
215
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
|
+
}
|
|
216
229
|
.sr-status {
|
|
217
230
|
position: fixed;
|
|
218
|
-
right:
|
|
219
|
-
|
|
231
|
+
right: 42px;
|
|
232
|
+
top: 50%;
|
|
233
|
+
transform: translateY(-50%);
|
|
220
234
|
z-index: 401;
|
|
221
235
|
font-size: 11px;
|
|
222
|
-
|
|
236
|
+
line-height: 1.5;
|
|
237
|
+
padding: 4px 9px;
|
|
223
238
|
border-radius: 6px;
|
|
224
|
-
background: var(--background-strong, rgba(0,0,0,.
|
|
239
|
+
background: var(--background-strong, rgba(0,0,0,.82));
|
|
225
240
|
color: var(--text-base, #fff);
|
|
226
|
-
|
|
227
|
-
pointer-events: none;
|
|
241
|
+
border: 1px solid rgba(127,127,127,.25);
|
|
228
242
|
white-space: pre-wrap;
|
|
229
|
-
max-width:
|
|
243
|
+
max-width: 260px;
|
|
230
244
|
}
|
|
231
|
-
`;function
|
|
245
|
+
`;function xt(a){const s=q.usePlugin(),[d,p]=l.createSignal([]),[S,$]=l.createSignal(""),[E,I]=l.createSignal(!1),[Je,Oe]=l.createSignal(""),[L,ge]=l.createSignal(""),[x,B]=l.createSignal(!1),[he,D]=l.createSignal(0),[me,P]=l.createSignal(0),[yt,J]=l.createSignal(0),[O,N]=l.createSignal(""),_=new Map;let W=0,H=!1,z=!1,V="",j;const v=()=>a.session()?.id,Y=()=>{const e=[..._.values()].sort((r,i)=>r.created-i.created),t=l.untrack(d);t.length===e.length&&t.every((r,i)=>r===e[i])||(p(e),Be=e.length)},be=e=>{const t=_.get(e.id);t&&t.created===e.time&&t.text===e.text||_.set(e.id,{id:e.id,created:e.time,text:e.text})},Ne=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 s.client.message.list(i)},K=async(e,t)=>{const r=++W;H=!0;let i="";try{let n;const c=t==="full"?ot:1,u=t==="full"?at:it;let g=t!=="full";for(let b=0;b<c;b++){let m;try{m=await Ne(e,u,n)}catch(h){i=String(h?.message??h);break}if(r!==W)return;g||(_.clear(),g=!0);let w=0;for(const h of m?.data??[])_.has(h.id)||(be({id:h.id,time:h.time?.created??0,text:je(h)}),w++);Y();const f=m?.cursor?.next;if(!f||w===0)break;n=f}}finally{H=!1,V=e,de=i,Oe(_.size===0?i||"暂无用户消息":""),z&&(z=!1,we())}},we=()=>{j||(j=setTimeout(()=>{j=void 0;const e=l.untrack(v);e&&K(e,"light")},300))},xe=e=>{if(!(!e||e!==l.untrack(v))){if(H){z=!0;return}we()}},He=e=>{const t=e?.type;if(t==="message.updated"){const r=e?.data??{},i=r.info;i?.role==="user"&&i?.id&&(be({id:i.id,time:i.time?.created??0,text:je(i)}),Y()),xe(r.sessionID)}else if(t==="message.removed"){const r=e?.data??{};r.messageID&&_.delete(r.messageID)&&Y(),xe(r.sessionID)}else if(t==="location.shutdown"){const r=l.untrack(v);r&&V===r&&K(r,"full")}};l.onMount(()=>{let e=!1,t=0,r;(async()=>{for(;!e;){const c=s.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!e;){const u=await c.next();if(u.done)break;t=0,He(u.value)}}catch{}try{await c.return?.()}catch{}if(e)break;t++,await se(Math.min(2e3*t,15e3))}})(),l.onCleanup(()=>{e=!0,clearTimeout(r),j&&clearTimeout(j)})}),l.createEffect(()=>{const e=v();e&&l.untrack(()=>{V!==e&&(W++,H=!1,z=!1,_.clear(),p([]),$(""),X="",K(e,"full"))})});let F;const C=()=>(F?.isConnected||(F=fe()),F),ze=()=>{const e=C();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 Z=!1;const ve=()=>{Z||(Z=!0,requestAnimationFrame(()=>{Z=!1,ze()}))};l.onMount(()=>{const e=()=>ve();document.addEventListener("scroll",e,!0);const t=setInterval(ve,1500);l.onCleanup(()=>{document.removeEventListener("scroll",e,!0),clearInterval(t)})});let ye=0;const U=(e,t=2600)=>{const r=++ye;ge(e),setTimeout(()=>{r===ye&&ge("")},t)};l.onMount(()=>{const e=r=>{if(r.key==="Escape"){if(l.untrack(x)){Ee(),r.stopPropagation();return}l.untrack(E)&&(I(!1),r.stopPropagation())}},t=r=>{if(!l.untrack(E))return;(r.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||I(!1)};window.addEventListener("keydown",e,!0),document.addEventListener("pointerdown",t,!0),l.onCleanup(()=>{window.removeEventListener("keydown",e,!0),document.removeEventListener("pointerdown",t,!0)})}),l.onMount(()=>{const e=()=>U(`${De} · ${oe(s)}`),t=()=>{I(r=>!r),Ce()};window.addEventListener(Pe,e),window.addEventListener(ce,t),l.onCleanup(()=>{window.removeEventListener(Pe,e),window.removeEventListener(ce,t)})});let X="",T=0,ke=0,Se="";const Q=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{}},Fe=()=>{for(const e of d())if(y(e.id))return e},ee=(e,t)=>{bt(`${e}#${le}${t}`),X=t},$e=()=>{const t=C()?.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}`},Ue=(e,t,r,i)=>new Promise(n=>{let c=!1,u,g,b;const m=Date.now();let f=$e(),h=m,A=0;const k=G=>{if(!c){c=!0;try{u?.disconnect()}catch{}clearInterval(g),clearTimeout(b),n(G??null)}},M=G=>{if(t!==T)return k(null);const Ae=G?y(e):document.getElementById(`${le}${e}`);if(Ae)return k(Ae);if(!G)return;const Me=$e(),Ie=Date.now();if(Me!==f){f=Me,h=Ie,A++,i?.(A);return}const Ze=A>0?ut:pt;Ie-h>Ze&&k(null)},Te=y(e);if(Te)return k(Te);u=new MutationObserver(()=>M(!1)),u.observe(document.documentElement,{childList:!0,subtree:!0}),g=setInterval(()=>M(!0),120),b=setTimeout(()=>k(y(e)),Math.max(400,r))}),Ee=()=>{T++,B(!1),D(0),P(0),J(0),N(""),U("已取消跳转")},Ge=async e=>{if(te()&&Ve(),x()&&O()===e.id)return;const t=++T;B(!1),D(0),P(0),J(0),N(""),pe=e.id;const r=Date.now();if(e.id===Se&&r-ke<gt){const w=y(e.id),f=C();if(w&&f){ie(w),Q(w);return}}ke=r,Se=e.id;const i=C(),n=y(e.id);if(n&&i){ie(n),Q(n);return}const c=qe(s);if(!c){U(`无法读取当前路由,无法定位 · ${oe(s)}`);return}const u=v();B(!0),N(e.id),D(0),P(0),I(!0);const g=Date.now(),b=setInterval(()=>{P(Math.round((Date.now()-g)/1e3))},500);let m=0;try{const w=Date.now()+ct;for(let f=0;f<lt&&Date.now()<w;f++){if(t!==T||v()!==u)return;if(J(f+1),X===e.id){const M=Fe()??d()[0];M&&(ee(c,M.id),await se(ft))}if(t!==T||v()!==u)return;ee(c,e.id);let h=0;const A=Math.min(dt,w-Date.now()),k=await Ue(e.id,t,A,M=>{h=M,D(m+M)});if(m+=h,t!==T||v()!==u)return;if(k){ie(k),Q(k);return}}t===T&&U(`未能定位 · ${oe(s)}`)}finally{clearInterval(b),t===T&&(B(!1),D(0),P(0),J(0),N(""))}},[te,re]=l.createSignal(!1);let R=0,ne="";const We=()=>{const e=C();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}}},_e=e=>{if(!e)return;const t=C();if(!t)return;const r=e.el?.isConnected?e.el:y(e.id);if(!r)return;const i=t.getBoundingClientRect(),n=r.getBoundingClientRect().top-i.top-e.offset;Math.abs(n)>2&&(t.scrollTop+=n)},Ve=()=>{R++,re(!1)},Ce=()=>{const e=v();!e||te()||ne!==e&&(d().length<15||Ye())},Ye=async()=>{if(te())return;const e=qe(s),t=d()[0],r=v();if(!e||!t||!r||ne===r&&y(t.id))return;const i=++R,n=We(),c=C(),u=c?c.scrollTop+c.clientHeight>=c.scrollHeight-32:!0;re(!0);try{ee(e,t.id);const g=Date.now()+12e4,b=setInterval(()=>_e(n),150);try{for(;Date.now()<g&&i===R&&!y(t.id);)await se(250)}finally{clearInterval(b)}if(i!==R)return;if(_e(n),u){const m=C();m&&(m.scrollTop=m.scrollHeight)}ne=r}finally{i===R&&re(!1)}};l.createEffect(()=>{x()&&O()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Ke=[o.createComponent(l.Show,{get when(){return L()},get children(){var e=Le();return o.insert(e,L),e}}),(()=>{var e=et(),t=e.firstChild,r=t.firstChild,i=t.nextSibling;return e.addEventListener("mouseleave",()=>{x()||I(!1)}),e.addEventListener("mouseenter",()=>{I(!0),Ce()}),o.insert(t,o.createComponent(l.Show,{get when(){return x()},get fallback(){return(()=>{var n=tt();return n.firstChild,o.insert(n,(()=>{var c=o.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),n})()},get children(){var n=Xe();return n.firstChild,o.insert(n,(()=>{var c=o.memo(()=>he()>0);return()=>c()?` · ${he()} 页`:""})(),null),o.insert(n,(()=>{var c=o.memo(()=>me()>0);return()=>c()?` · ${me()}s`:""})(),null),n}}),r),r.$$click=()=>{x()?Ee():I(!1)},o.insert(i,o.createComponent(l.Show,{get when(){return d().length>0},get fallback(){return(()=>{var n=rt();return o.insert(n,()=>Je()||`… ${De}`),n})()},get children(){return o.createComponent(l.For,{get each(){return d()},children:(n,c)=>(()=>{var u=st(),g=u.firstChild,b=g.firstChild;b.firstChild;var m=b.nextSibling,w=g.nextSibling;return u.$$click=()=>{Ge(n)},o.insert(b,()=>c()+1,null),o.insert(m,()=>ht(n.created)),o.insert(g,()=>n.text,null),o.insert(u,o.createComponent(l.Show,{get when(){return o.memo(()=>!!x())()&&O()===n.id},get children(){return nt()}}),w),o.effect(f=>{var h=S()===n.id?"true":"false",A=x()&&O()===n.id?"true":"false";return h!==f.e&&o.setAttribute(u,"data-active",f.e=h),A!==f.t&&o.setAttribute(u,"data-busy",f.t=A),f},{e:void 0,t:void 0}),u})()})}})),o.insert(e,o.createComponent(l.Show,{get when(){return o.memo(()=>!!E())()&&L()},get children(){var n=Qe();return o.insert(n,L),n}}),null),o.effect(n=>{var c=E()?"true":"false",u=x()?"true":"false",g=x()?"取消跳转 (Esc)":"关闭 (Esc)";return c!==n.e&&o.setAttribute(e,"data-open",n.e=c),u!==n.t&&o.setAttribute(e,"data-jumping",n.t=u),g!==n.a&&o.setAttribute(r,"title",n.a=g),n},{e:void 0,t:void 0,a:void 0}),e})(),o.createComponent(l.Show,{get when(){return o.memo(()=>!E())()&&L()},get children(){var e=Le();return o.insert(e,L),e}})];return o.createComponent(o.Portal,{get mount(){return document.body},children:Ke})}const vt=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(a){a.add(q.Style,wt),a.add(q.Slot,{at:"window.bottom",render:()=>o.createComponent(l.Show,{get when(){return a.sessions.current()},get children(){return o.createComponent(xt,{session:()=>a.sessions.current()})}})}),a.add(q.Command,()=>({id:"jump",title:a.t("command.jump"),description:a.t("command.jump.description"),group:a.t("command.jump"),section:"navigation",bind:"mod+alt+r",slash:{name:"turns"},run:()=>{window.dispatchEvent(new Event(ce))}}))}});o.delegateEvents(["click"]);exports.default=vt;
|
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,21 +253,10 @@ 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
|
-
.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
/* 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. */
|
|
262
260
|
.sr-list {
|
|
263
261
|
flex: 1;
|
|
264
262
|
min-height: 0;
|
|
@@ -266,11 +264,15 @@ const CSS = `
|
|
|
266
264
|
flex-direction: column;
|
|
267
265
|
justify-content: center;
|
|
268
266
|
gap: 2px;
|
|
269
|
-
overflow
|
|
270
|
-
overflow-x: hidden;
|
|
267
|
+
overflow: hidden;
|
|
271
268
|
scrollbar-width: none;
|
|
272
269
|
}
|
|
273
|
-
.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
|
+
}
|
|
274
276
|
.sr-list::-webkit-scrollbar { display: none; }
|
|
275
277
|
.sr-rail[data-open="true"] .sr-list::-webkit-scrollbar { display: block; width: 8px; }
|
|
276
278
|
.sr-list::-webkit-scrollbar-track { background: transparent; }
|
|
@@ -292,9 +294,6 @@ const CSS = `
|
|
|
292
294
|
color: inherit;
|
|
293
295
|
font: inherit;
|
|
294
296
|
border-radius: 4px;
|
|
295
|
-
/* long sessions: skip layout/paint for rows outside the panel viewport */
|
|
296
|
-
content-visibility: auto;
|
|
297
|
-
contain-intrinsic-size: 0 26px;
|
|
298
297
|
}
|
|
299
298
|
/* expanded: dash flips to a left-side indicator, content reads left-to-right */
|
|
300
299
|
.sr-rail[data-open="true"] .sr-turn {
|
|
@@ -390,6 +389,17 @@ const CSS = `
|
|
|
390
389
|
box-shadow: inset 0 0 0 2px rgba(255,170,60,0), inset 0 0 0 rgba(255,170,60,0);
|
|
391
390
|
}
|
|
392
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
|
+
}
|
|
393
403
|
.sr-note {
|
|
394
404
|
display: none;
|
|
395
405
|
font-size: 11px;
|
|
@@ -397,20 +407,33 @@ const CSS = `
|
|
|
397
407
|
padding: 2px;
|
|
398
408
|
}
|
|
399
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
|
+
}
|
|
400
422
|
.sr-status {
|
|
401
423
|
position: fixed;
|
|
402
|
-
right:
|
|
403
|
-
|
|
424
|
+
right: 42px;
|
|
425
|
+
top: 50%;
|
|
426
|
+
transform: translateY(-50%);
|
|
404
427
|
z-index: 401;
|
|
405
428
|
font-size: 11px;
|
|
406
|
-
|
|
429
|
+
line-height: 1.5;
|
|
430
|
+
padding: 4px 9px;
|
|
407
431
|
border-radius: 6px;
|
|
408
|
-
background: var(--background-strong, rgba(0,0,0,.
|
|
432
|
+
background: var(--background-strong, rgba(0,0,0,.82));
|
|
409
433
|
color: var(--text-base, #fff);
|
|
410
|
-
|
|
411
|
-
pointer-events: none;
|
|
434
|
+
border: 1px solid rgba(127,127,127,.25);
|
|
412
435
|
white-space: pre-wrap;
|
|
413
|
-
max-width:
|
|
436
|
+
max-width: 260px;
|
|
414
437
|
}
|
|
415
438
|
`
|
|
416
439
|
|
|
@@ -722,17 +745,13 @@ function Rail(props) {
|
|
|
722
745
|
setOpen((value) => !value)
|
|
723
746
|
maybePrewarm()
|
|
724
747
|
}
|
|
725
|
-
const onWarm = () => void prewarm()
|
|
726
748
|
window.addEventListener(DIAG_EVENT, onDiag)
|
|
727
749
|
window.addEventListener(OPEN_EVENT, onOpen)
|
|
728
|
-
window.addEventListener(WARM_EVENT, onWarm)
|
|
729
750
|
onCleanup(() => {
|
|
730
751
|
window.removeEventListener(DIAG_EVENT, onDiag)
|
|
731
752
|
window.removeEventListener(OPEN_EVENT, onOpen)
|
|
732
|
-
window.removeEventListener(WARM_EVENT, onWarm)
|
|
733
753
|
})
|
|
734
|
-
//
|
|
735
|
-
flash(`会话轮次 ${VERSION} 已加载`)
|
|
754
|
+
// No startup toast: the plugin should be invisible when everything works.
|
|
736
755
|
})
|
|
737
756
|
|
|
738
757
|
// ---- jump ----------------------------------------------------------------
|
|
@@ -860,12 +879,9 @@ function Rail(props) {
|
|
|
860
879
|
|
|
861
880
|
// Clicking the same turn again while its jump is running must NOT restart
|
|
862
881
|
// the loop: every restart cancels the host's in-flight page load, which is
|
|
863
|
-
// what makes a deep jump advance only one page per click.
|
|
864
|
-
// progress
|
|
865
|
-
if (jumping() && busyID() === turn.id)
|
|
866
|
-
flash(`正在跳转 · 已加载 ${jumpPages()} 页 · ${jumpElapsed()}s`, JUMP_TOTAL_MS)
|
|
867
|
-
return
|
|
868
|
-
}
|
|
882
|
+
// what makes a deep jump advance only one page per click. The header
|
|
883
|
+
// already shows the progress, so just leave it running.
|
|
884
|
+
if (jumping() && busyID() === turn.id) return
|
|
869
885
|
|
|
870
886
|
const token = ++jumpToken
|
|
871
887
|
// A new click always owns the progress UI. This also clears a previous
|
|
@@ -896,11 +912,11 @@ function Rail(props) {
|
|
|
896
912
|
const sc = scroller()
|
|
897
913
|
const row = findRow(turn.id)
|
|
898
914
|
|
|
899
|
-
// Fast path: the row is mounted, scroll locally.
|
|
915
|
+
// Fast path: the row is mounted, scroll locally. No toast: the bubble
|
|
916
|
+
// ring is the feedback.
|
|
900
917
|
if (row && sc) {
|
|
901
918
|
alignAfterLand(row)
|
|
902
919
|
pulse(row)
|
|
903
|
-
flash("已定位")
|
|
904
920
|
return
|
|
905
921
|
}
|
|
906
922
|
|
|
@@ -929,7 +945,8 @@ function Rail(props) {
|
|
|
929
945
|
setJumpElapsed(0)
|
|
930
946
|
// Pin the panel open even if the pointer has left it meanwhile.
|
|
931
947
|
setOpen(true)
|
|
932
|
-
|
|
948
|
+
// The loading state is shown in the panel header and on the target row -
|
|
949
|
+
// no toast needed, the toast would land outside the user's focus.
|
|
933
950
|
const startedAt = Date.now()
|
|
934
951
|
const elapsedTimer = setInterval(() => {
|
|
935
952
|
setJumpElapsed(Math.round((Date.now() - startedAt) / 1000))
|
|
@@ -959,21 +976,19 @@ function Rail(props) {
|
|
|
959
976
|
}
|
|
960
977
|
if (token !== jumpToken || sessionID() !== sessionAtJump) return
|
|
961
978
|
navTo(path, turn.id)
|
|
962
|
-
flash(`跳转中 · 已加载 ${chainBase} 页`, JUMP_TOTAL_MS)
|
|
963
979
|
|
|
964
980
|
let chainPages = 0
|
|
965
981
|
const remaining = Math.min(JUMP_CHAIN_MS, deadline - Date.now())
|
|
966
982
|
const landed = await waitForRow(turn.id, token, remaining, (pages) => {
|
|
967
983
|
chainPages = pages
|
|
984
|
+
// Progress is shown in the panel header, not in a toast.
|
|
968
985
|
setJumpPages(chainBase + pages)
|
|
969
|
-
flash(`跳转中 · 已加载 ${chainBase + pages} 页`, JUMP_TOTAL_MS)
|
|
970
986
|
})
|
|
971
987
|
chainBase += chainPages
|
|
972
988
|
if (token !== jumpToken || sessionID() !== sessionAtJump) return
|
|
973
989
|
if (landed) {
|
|
974
990
|
alignAfterLand(landed)
|
|
975
991
|
pulse(landed)
|
|
976
|
-
flash("已定位")
|
|
977
992
|
return
|
|
978
993
|
}
|
|
979
994
|
// The host went idle without landing: it exhausted this chain. The
|
|
@@ -1002,7 +1017,6 @@ function Rail(props) {
|
|
|
1002
1017
|
// and restore the viewport afterwards. Once the store holds the whole
|
|
1003
1018
|
// history every jump is instant.
|
|
1004
1019
|
const [warming, setWarming] = createSignal(false)
|
|
1005
|
-
const [warmPages, setWarmPages] = createSignal(0)
|
|
1006
1020
|
let warmToken = 0
|
|
1007
1021
|
let warmedFor = ""
|
|
1008
1022
|
|
|
@@ -1014,9 +1028,9 @@ function Rail(props) {
|
|
|
1014
1028
|
for (const row of sc.querySelectorAll("[data-message-id]")) {
|
|
1015
1029
|
const rect = row.getBoundingClientRect()
|
|
1016
1030
|
if (rect.bottom <= scRect.top + 1) continue
|
|
1017
|
-
const
|
|
1018
|
-
if (!
|
|
1019
|
-
return { id, offset: rect.top - scRect.top }
|
|
1031
|
+
const mid = row.getAttribute("data-message-id")
|
|
1032
|
+
if (!mid) continue
|
|
1033
|
+
return { el: row, id: mid, offset: rect.top - scRect.top }
|
|
1020
1034
|
}
|
|
1021
1035
|
return undefined
|
|
1022
1036
|
}
|
|
@@ -1025,7 +1039,9 @@ function Rail(props) {
|
|
|
1025
1039
|
if (!anchor) return
|
|
1026
1040
|
const sc = scroller()
|
|
1027
1041
|
if (!sc) return
|
|
1028
|
-
|
|
1042
|
+
// Prefer the element itself: an assistant row can carry the same message
|
|
1043
|
+
// id, so looking it up by id might land on a different element.
|
|
1044
|
+
const row = anchor.el?.isConnected ? anchor.el : findRow(anchor.id)
|
|
1029
1045
|
if (!row) return
|
|
1030
1046
|
const scRect = sc.getBoundingClientRect()
|
|
1031
1047
|
const delta = row.getBoundingClientRect().top - scRect.top - anchor.offset
|
|
@@ -1054,33 +1070,20 @@ function Rail(props) {
|
|
|
1054
1070
|
const sid = sessionID()
|
|
1055
1071
|
if (!path || !oldest || !sid) return
|
|
1056
1072
|
// 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
|
-
}
|
|
1073
|
+
if (warmedFor === sid && findRow(oldest.id)) return
|
|
1061
1074
|
const token = ++warmToken
|
|
1062
1075
|
const anchor = viewAnchor()
|
|
1063
1076
|
const sc = scroller()
|
|
1064
1077
|
const wasAtBottom = sc ? sc.scrollTop + sc.clientHeight >= sc.scrollHeight - 32 : true
|
|
1065
1078
|
setWarming(true)
|
|
1066
|
-
setWarmPages(0)
|
|
1067
|
-
flash("正在预热历史…", 120000)
|
|
1068
1079
|
try {
|
|
1069
1080
|
navTo(path, oldest.id)
|
|
1070
1081
|
const deadline = Date.now() + 120000
|
|
1071
|
-
let pages = 0
|
|
1072
|
-
let lastSig = rowSignature()
|
|
1073
1082
|
const keep = setInterval(() => restoreView(anchor), 150)
|
|
1074
1083
|
try {
|
|
1075
1084
|
while (Date.now() < deadline && token === warmToken) {
|
|
1076
1085
|
if (findRow(oldest.id)) break
|
|
1077
1086
|
await sleep(250)
|
|
1078
|
-
const sig = rowSignature()
|
|
1079
|
-
if (sig !== lastSig) {
|
|
1080
|
-
lastSig = sig
|
|
1081
|
-
pages++
|
|
1082
|
-
setWarmPages(pages)
|
|
1083
|
-
}
|
|
1084
1087
|
}
|
|
1085
1088
|
} finally {
|
|
1086
1089
|
clearInterval(keep)
|
|
@@ -1093,7 +1096,7 @@ function Rail(props) {
|
|
|
1093
1096
|
if (live) live.scrollTop = live.scrollHeight
|
|
1094
1097
|
}
|
|
1095
1098
|
warmedFor = sid
|
|
1096
|
-
|
|
1099
|
+
// Silent on success: prewarming is an implementation detail.
|
|
1097
1100
|
} finally {
|
|
1098
1101
|
if (token === warmToken) setWarming(false)
|
|
1099
1102
|
}
|
|
@@ -1135,25 +1138,16 @@ function Rail(props) {
|
|
|
1135
1138
|
>
|
|
1136
1139
|
<div class="sr-head">
|
|
1137
1140
|
<Show
|
|
1138
|
-
when={jumping()
|
|
1141
|
+
when={jumping()}
|
|
1139
1142
|
fallback={
|
|
1140
1143
|
<span class="sr-title">会话轮次{turns().length > 0 ? ` · ${turns().length}` : ""}</span>
|
|
1141
1144
|
}
|
|
1142
1145
|
>
|
|
1143
1146
|
<span class="sr-title sr-progress">
|
|
1144
|
-
{
|
|
1145
|
-
|
|
1146
|
-
: `预热中${warmPages() > 0 ? ` · ${warmPages()} 页` : ""}`}
|
|
1147
|
+
加载历史中{jumpPages() > 0 ? ` · ${jumpPages()} 页` : ""}
|
|
1148
|
+
{jumpElapsed() > 0 ? ` · ${jumpElapsed()}s` : ""}
|
|
1147
1149
|
</span>
|
|
1148
1150
|
</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
1151
|
<button
|
|
1158
1152
|
type="button"
|
|
1159
1153
|
class="sr-close"
|
|
@@ -1182,13 +1176,25 @@ function Rail(props) {
|
|
|
1182
1176
|
<span class="sr-time">{formatTime(turn.created)}</span>
|
|
1183
1177
|
{turn.text}
|
|
1184
1178
|
</span>
|
|
1179
|
+
<Show when={jumping() && busyID() === turn.id}>
|
|
1180
|
+
<span class="sr-badge">加载中</span>
|
|
1181
|
+
</Show>
|
|
1185
1182
|
<span class="sr-dash" />
|
|
1186
1183
|
</button>
|
|
1187
1184
|
)}
|
|
1188
1185
|
</For>
|
|
1189
1186
|
</Show>
|
|
1190
1187
|
</div>
|
|
1188
|
+
{/* Messages that need attention (errors, cancel confirmations) live in
|
|
1189
|
+
the panel footer while it is open, and next to the collapsed rail
|
|
1190
|
+
when it is closed - the old bottom-right toast was easy to miss. */}
|
|
1191
|
+
<Show when={open() && status()}>
|
|
1192
|
+
<div class="sr-foot">{status()}</div>
|
|
1193
|
+
</Show>
|
|
1191
1194
|
</div>
|
|
1195
|
+
<Show when={!open() && status()}>
|
|
1196
|
+
<div class="sr-status">{status()}</div>
|
|
1197
|
+
</Show>
|
|
1192
1198
|
</>
|
|
1193
1199
|
)
|
|
1194
1200
|
|