opencode-session-turns 0.1.8 → 0.1.9
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 +6 -3
- package/package.json +1 -1
- package/src/gui.tsx +46 -31
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("solid-js/web"),q=require("@opencode/plugin/gui"),i=require("solid-js");var
|
|
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"),i=require("solid-js");var zt=a.template("<div class=sr-status>"),Ft=a.template('<span class="sr-title sr-progress">跳转中'),Ut=a.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),Gt=a.template("<span class=sr-title>会话轮次"),Ht=a.template("<div class=sr-note>"),Wt=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 et="v13",ot="message-",Ct="session-turns:diag",at="session-turns:open",Vt=40,Kt=200,Yt=50,Zt=30,j=45e3,Xt=12e3,Qt=5e3,te=9e3,ee=220,ne=800;let Lt=0,it="",lt="",ct="";const At=s=>{const n=String(s?.text??"").split(`
|
|
2
|
+
`).map(p=>p.trim()).find(p=>p.length>0);if(n)return n;const d=s?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},Tt=s=>String(s).padStart(2,"0"),re=s=>{const n=new Date(s??0);return`${Tt(n.getHours())}:${Tt(n.getMinutes())}`},Mt=s=>new Promise(n=>setTimeout(n,s)),dt=()=>document.querySelector('[data-slot="session-timeline-scroll"] > .scroll-view__viewport')??document.querySelector('[data-slot="session-timeline-scroll"] [data-scrollable]')??(()=>{let n=document.querySelector("[data-message-id]")?.parentElement;for(;n;){const d=getComputedStyle(n);if(/(auto|scroll)/.test(d.overflowY)&&n.scrollHeight>n.clientHeight+1)return n;n=n.parentElement}})(),T=s=>document.getElementById(`${ot}${s}`)??[...document.querySelectorAll("[data-message-id]")].find(n=>n.getAttribute("data-message-id")===s),nt=s=>{const n=dt(),d=[...document.querySelectorAll("[data-message-id]")],p=d[0]?.getAttribute("data-message-id")??"",$=ct?` · 目标 ${T(ct)?"在":"不在"} DOM`:"",S=p?` · 行ID ${p.slice(0,20)}`:"";let M="(空)";try{M=s.router.path()||"(空)"}catch{}return`轮次 ${Lt} · 容器 ${n?"OK":"缺失"} · DOM ${d.length} 行${$}${S} · 路由 ${M} · hash ${window.location.hash||"(空)"}${lt?` · ${lt}`:""}${it?` · ${it}`:""}`},se=s=>{const n=s.getBoundingClientRect().top;let d=n;for(const p of s.querySelectorAll(".sticky, [data-session-title]")){if(!(p instanceof HTMLElement)||getComputedStyle(p).position!=="sticky"&&!p.hasAttribute("data-session-title"))continue;const S=p.getBoundingClientRect();S.height!==0&&(S.top>n+4||(d=Math.max(d,S.bottom)))}return d},rt=s=>{try{const n=dt();if(!n||!s.isConnected)return;const d=s.querySelector('[data-slot="user-message-text"]')??s,p=se(n),$=d.getBoundingClientRect().top-(p+18);Math.abs($)>2&&(n.scrollTop+=$),lt=`inset ${Math.round(p)} · delta ${Math.round($)}`}catch{}},st=s=>{rt(s),setTimeout(()=>rt(s),450),setTimeout(()=>rt(s),1e3)},oe=s=>{let n="";try{n=s.router.path()||""}catch{}return n.startsWith("/")?n:""},ae=s=>{const n=document.createElement("a");n.setAttribute("href",s),n.setAttribute("replace",""),n.setAttribute("noscroll",""),n.style.display="none",document.body.appendChild(n);try{n.dispatchEvent(new MouseEvent("click",{bubbles:!0,cancelable:!0,button:0,view:window}))}finally{n.remove()}},ie=`
|
|
3
3
|
.sr-rail {
|
|
4
4
|
position: fixed;
|
|
5
5
|
top: 50%;
|
|
@@ -94,6 +94,9 @@
|
|
|
94
94
|
color: inherit;
|
|
95
95
|
font: inherit;
|
|
96
96
|
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;
|
|
97
100
|
}
|
|
98
101
|
/* expanded: dash flips to a left-side indicator, content reads left-to-right */
|
|
99
102
|
.sr-rail[data-open="true"] .sr-turn {
|
|
@@ -215,4 +218,4 @@
|
|
|
215
218
|
white-space: pre-wrap;
|
|
216
219
|
max-width: min(720px, 90vw);
|
|
217
220
|
}
|
|
218
|
-
`;function
|
|
221
|
+
`;function le(s){const n=q.usePlugin(),[d,p]=i.createSignal([]),[$,S]=i.createSignal(""),[M,A]=i.createSignal(!1),[It,Dt]=i.createSignal(""),[ut,pt]=i.createSignal(""),[w,R]=i.createSignal(!1),[G,L]=i.createSignal(0),[H,I]=i.createSignal(0),[de,J]=i.createSignal(0),[W,B]=i.createSignal(""),E=new Map;let V=0,O=!1,N=!1,K="",D;const _=()=>s.session()?.id,Y=()=>{const t=[...E.values()].sort((r,l)=>r.created-l.created),e=i.untrack(d);e.length===t.length&&e.every((r,l)=>r===t[l])||(p(t),Lt=t.length)},ft=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})},Pt=async(t,e,r)=>{const l=r?{sessionID:t,type:"user",limit:e,cursor:r}:{sessionID:t,type:"user",limit:e,order:"desc"};return await n.client.message.list(l)},Z=async(t,e)=>{const r=++V;O=!0;let l="";try{let o;const c=e==="full"?Vt:1,u=e==="full"?Kt:Yt;let m=e!=="full";for(let v=0;v<c;v++){let b;try{b=await Pt(t,u,o)}catch(h){l=String(h?.message??h);break}if(r!==V)return;m||(E.clear(),m=!0);let f=0;for(const h of b?.data??[])E.has(h.id)||(ft({id:h.id,time:h.time?.created??0,text:At(h)}),f++);Y();const g=b?.cursor?.next;if(!g||f===0)break;o=g}}finally{O=!1,K=t,it=l,Dt(E.size===0?l||"暂无用户消息":""),N&&(N=!1,ht())}},ht=()=>{D||(D=setTimeout(()=>{D=void 0;const t=i.untrack(_);t&&Z(t,"light")},300))},gt=t=>{if(!(!t||t!==i.untrack(_))){if(O){N=!0;return}ht()}},jt=t=>{const e=t?.type;if(e==="message.updated"){const r=t?.data??{},l=r.info;l?.role==="user"&&l?.id&&(ft({id:l.id,time:l.time?.created??0,text:At(l)}),Y()),gt(r.sessionID)}else if(e==="message.removed"){const r=t?.data??{};r.messageID&&E.delete(r.messageID)&&Y(),gt(r.sessionID)}else if(e==="location.shutdown"){const r=i.untrack(_);r&&K===r&&Z(r,"full")}};i.onMount(()=>{let t=!1,e=0,r;(async()=>{for(;!t;){const c=n.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!t;){const u=await c.next();if(u.done)break;e=0,jt(u.value)}}catch{}try{await c.return?.()}catch{}if(t)break;e++,await Mt(Math.min(2e3*e,15e3))}})(),i.onCleanup(()=>{t=!0,clearTimeout(r),D&&clearTimeout(D)})}),i.createEffect(()=>{const t=_();t&&i.untrack(()=>{K!==t&&(V++,O=!1,N=!1,E.clear(),p([]),S(""),Q="",Z(t,"full"))})});let z;const F=()=>(z?.isConnected||(z=dt()),z),qt=()=>{const t=F();if(!t)return;const e=t.querySelectorAll("[data-message-id]");if(e.length===0)return;const r=t.getBoundingClientRect().top+40;for(const l of e)if(l.getBoundingClientRect().bottom>=r){const o=l.getAttribute("data-message-id");o&&S(o);break}};let X=!1;const mt=()=>{X||(X=!0,requestAnimationFrame(()=>{X=!1,qt()}))};i.onMount(()=>{const t=()=>mt();document.addEventListener("scroll",t,!0);const e=setInterval(mt,1500);i.onCleanup(()=>{document.removeEventListener("scroll",t,!0),clearInterval(e)})});let bt=0;const x=(t,e=2600)=>{const r=++bt;pt(t),setTimeout(()=>{r===bt&&pt("")},e)};i.onMount(()=>{const t=r=>{if(r.key==="Escape"){if(i.untrack(w)){kt(),r.stopPropagation();return}i.untrack(M)&&(A(!1),r.stopPropagation())}},e=r=>{if(!i.untrack(M))return;(r.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||A(!1)};window.addEventListener("keydown",t,!0),document.addEventListener("pointerdown",e,!0),i.onCleanup(()=>{window.removeEventListener("keydown",t,!0),document.removeEventListener("pointerdown",e,!0)})}),i.onMount(()=>{const t=()=>x(`${et} · ${nt(n)}`),e=()=>A(r=>!r);window.addEventListener(Ct,t),window.addEventListener(at,e),i.onCleanup(()=>{window.removeEventListener(Ct,t),window.removeEventListener(at,e)}),x(`会话轮次 ${et} 已加载`)});let Q="",C=0,xt=0,vt="";const tt=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{}},Rt=()=>{for(const t of d())if(T(t.id))return t},wt=(t,e)=>{ae(`${t}#${ot}${e}`),Q=e},yt=()=>{const e=F()?.querySelectorAll("[data-message-id]")??[];if(e.length===0)return"0";const r=e[0].getAttribute("data-message-id")??"",l=e[e.length-1].getAttribute("data-message-id")??"";return`${e.length}:${r}:${l}`},Jt=(t,e,r,l)=>new Promise(o=>{let c=!1,u,m,v;const b=Date.now();let g=yt(),h=b,P=0;const y=U=>{if(!c){c=!0;try{u?.disconnect()}catch{}clearInterval(m),clearTimeout(v),o(U??null)}},k=U=>{if(e!==C)return y(null);const St=U?T(t):document.getElementById(`${ot}${t}`);if(St)return y(St);if(!U)return;const Et=yt(),_t=Date.now();if(Et!==g){g=Et,h=_t,P++,l?.(P);return}const Nt=P>0?Qt:te;_t-h>Nt&&y(null)},$t=T(t);if($t)return y($t);u=new MutationObserver(()=>k(!1)),u.observe(document.documentElement,{childList:!0,subtree:!0}),m=setInterval(()=>k(!0),120),v=setTimeout(()=>y(T(t)),Math.max(400,r))}),kt=()=>{C++,R(!1),L(0),I(0),J(0),B(""),x("已取消跳转")},Bt=async t=>{if(w()&&W()===t.id){x(`正在跳转 · 已加载 ${G()} 页 · ${H()}s`,j);return}const e=++C;R(!1),L(0),I(0),J(0),B(""),ct=t.id;const r=Date.now();if(t.id===vt&&r-xt<ne){const f=T(t.id),g=F();if(f&&g){st(f),tt(f);return}}xt=r,vt=t.id;const l=F(),o=T(t.id);if(o&&l){st(o),tt(o),x("已定位");return}const c=oe(n);if(!c){x(`无法读取当前路由,无法定位 · ${nt(n)}`);return}const u=_();R(!0),B(t.id),L(0),I(0),A(!0),x("正在定位…",j);const m=Date.now(),v=setInterval(()=>{I(Math.round((Date.now()-m)/1e3))},500);let b=0;try{const f=Date.now()+j;for(let g=0;g<Zt&&Date.now()<f;g++){if(e!==C||_()!==u)return;if(J(g+1),Q===t.id){const k=Rt()??d()[0];k&&(wt(c,k.id),await Mt(ee))}if(e!==C||_()!==u)return;wt(c,t.id),x(`跳转中 · 已加载 ${b} 页`,j);let h=0;const P=Math.min(Xt,f-Date.now()),y=await Jt(t.id,e,P,k=>{h=k,L(b+k),x(`跳转中 · 已加载 ${b+k} 页`,j)});if(b+=h,e!==C||_()!==u)return;if(y){st(y),tt(y),x("已定位");return}}e===C&&x(`未能定位 · ${nt(n)}`)}finally{clearInterval(v),e===C&&(R(!1),L(0),I(0),J(0),B(""))}};i.createEffect(()=>{w()&&W()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Ot=[a.createComponent(i.Show,{get when(){return ut()},get children(){var t=zt();return a.insert(t,ut),t}}),(()=>{var t=Ut(),e=t.firstChild,r=e.firstChild,l=e.nextSibling;return t.addEventListener("mouseleave",()=>{w()||A(!1)}),t.addEventListener("mouseenter",()=>A(!0)),a.insert(e,a.createComponent(i.Show,{get when(){return w()},get fallback(){return(()=>{var o=Gt();return o.firstChild,a.insert(o,(()=>{var c=a.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),o})()},get children(){var o=Ft();return o.firstChild,a.insert(o,(()=>{var c=a.memo(()=>G()>0);return()=>c()?` · 已加载 ${G()} 页`:""})(),null),a.insert(o,(()=>{var c=a.memo(()=>H()>0);return()=>c()?` · ${H()}s`:""})(),null),o}}),r),r.$$click=()=>{w()?kt():A(!1)},a.insert(l,a.createComponent(i.Show,{get when(){return d().length>0},get fallback(){return(()=>{var o=Ht();return a.insert(o,()=>It()||`… ${et}`),o})()},get children(){return a.createComponent(i.For,{get each(){return d()},children:(o,c)=>(()=>{var u=Wt(),m=u.firstChild,v=m.firstChild;v.firstChild;var b=v.nextSibling;return u.$$click=()=>{Bt(o)},a.insert(v,()=>c()+1,null),a.insert(b,()=>re(o.created)),a.insert(m,()=>o.text,null),a.effect(f=>{var g=$()===o.id?"true":"false",h=w()&&W()===o.id?"true":"false";return g!==f.e&&a.setAttribute(u,"data-active",f.e=g),h!==f.t&&a.setAttribute(u,"data-busy",f.t=h),f},{e:void 0,t:void 0}),u})()})}})),a.effect(o=>{var c=M()?"true":"false",u=w()?"true":"false",m=w()?"取消跳转 (Esc)":"关闭 (Esc)";return c!==o.e&&a.setAttribute(t,"data-open",o.e=c),u!==o.t&&a.setAttribute(t,"data-jumping",o.t=u),m!==o.a&&a.setAttribute(r,"title",o.a=m),o},{e:void 0,t:void 0,a:void 0}),t})()];return a.createComponent(a.Portal,{get mount(){return document.body},children:Ot})}const ce=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(s){s.add(q.Style,ie),s.add(q.Slot,{at:"window.bottom",render:()=>a.createComponent(i.Show,{get when(){return s.sessions.current()},get children(){return a.createComponent(le,{session:()=>s.sessions.current()})}})}),s.add(q.Command,()=>({id:"jump",title:s.t("command.jump"),description:s.t("command.jump.description"),group:s.t("command.jump"),section:"navigation",bind:"mod+alt+r",slash:{name:"turns"},run:()=>{window.dispatchEvent(new Event(at))}}))}});a.delegateEvents(["click"]);exports.default=ce;
|
package/package.json
CHANGED
package/src/gui.tsx
CHANGED
|
@@ -35,6 +35,7 @@ const SAME_TARGET_WINDOW_MS = 800
|
|
|
35
35
|
// Filled by Rail.load()/jump() so the command palette can report diagnostics.
|
|
36
36
|
let lastTurnCount = 0
|
|
37
37
|
let lastNote = ""
|
|
38
|
+
let lastAlign = ""
|
|
38
39
|
let lastJumpID = ""
|
|
39
40
|
|
|
40
41
|
const firstLine = (message) => {
|
|
@@ -91,37 +92,50 @@ const diagnostics = (ctx) => {
|
|
|
91
92
|
try {
|
|
92
93
|
route = ctx.router.path() || "(空)"
|
|
93
94
|
} catch {}
|
|
94
|
-
return `轮次 ${lastTurnCount} · 容器 ${scroller ? "OK" : "缺失"} · DOM ${nodes.length} 行${target}${id} · 路由 ${route} · hash ${window.location.hash || "(空)"}${lastNote ? ` · ${lastNote}` : ""}`
|
|
95
|
+
return `轮次 ${lastTurnCount} · 容器 ${scroller ? "OK" : "缺失"} · DOM ${nodes.length} 行${target}${id} · 路由 ${route} · hash ${window.location.hash || "(空)"}${lastAlign ? ` · ${lastAlign}` : ""}${lastNote ? ` · ${lastNote}` : ""}`
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
98
|
+
// The timeline's top is covered by the host's sticky header: the session title
|
|
99
|
+
// (z-30) plus a 16px gradient fade strip that sits right below it. The host
|
|
100
|
+
// parks a sought row flush against that band, which is why the bubble's top
|
|
101
|
+
// edge (and the highlight ring) used to be cut off.
|
|
102
|
+
//
|
|
103
|
+
// Instead of guessing a fixed offset, measure the band: every sticky element
|
|
104
|
+
// pinned to the top of the scrollport contributes its bottom edge.
|
|
105
|
+
const topInset = (scroller) => {
|
|
106
|
+
const scTop = scroller.getBoundingClientRect().top
|
|
107
|
+
let bottom = scTop
|
|
108
|
+
for (const el of scroller.querySelectorAll(".sticky, [data-session-title]")) {
|
|
109
|
+
if (!(el instanceof HTMLElement)) continue
|
|
110
|
+
const style = getComputedStyle(el)
|
|
111
|
+
if (style.position !== "sticky" && !el.hasAttribute("data-session-title")) continue
|
|
112
|
+
const rect = el.getBoundingClientRect()
|
|
113
|
+
if (rect.height === 0) continue
|
|
114
|
+
if (rect.top > scTop + 4) continue // not pinned to the top
|
|
115
|
+
bottom = Math.max(bottom, rect.bottom)
|
|
116
|
+
}
|
|
117
|
+
return bottom
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const repairScroll = (row) => {
|
|
115
|
-
setTimeout(() => {
|
|
116
|
-
if (!row.isConnected) return
|
|
120
|
+
// Places the message bubble (not the row) clear of the header band.
|
|
121
|
+
const alignBubble = (row) => {
|
|
122
|
+
try {
|
|
117
123
|
const sc = findScroller()
|
|
118
|
-
if (!sc) return
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
124
|
+
if (!sc || !row.isConnected) return
|
|
125
|
+
const bubble = row.querySelector('[data-slot="user-message-text"]') ?? row
|
|
126
|
+
const inset = topInset(sc)
|
|
127
|
+
const delta = bubble.getBoundingClientRect().top - (inset + 18)
|
|
128
|
+
if (Math.abs(delta) > 2) sc.scrollTop += delta
|
|
129
|
+
lastAlign = `inset ${Math.round(inset)} · delta ${Math.round(delta)}`
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The host's own seek may finish after ours (it can even scroll smoothly),
|
|
134
|
+
// re-aligning the row flush under the header. Re-align a couple of times.
|
|
135
|
+
const alignAfterLand = (row) => {
|
|
136
|
+
alignBubble(row)
|
|
137
|
+
setTimeout(() => alignBubble(row), 450)
|
|
138
|
+
setTimeout(() => alignBubble(row), 1000)
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
// The desktop app (2.2.6) routes through a memory history, so window.location is
|
|
@@ -263,6 +277,9 @@ const CSS = `
|
|
|
263
277
|
color: inherit;
|
|
264
278
|
font: inherit;
|
|
265
279
|
border-radius: 4px;
|
|
280
|
+
/* long sessions: skip layout/paint for rows outside the panel viewport */
|
|
281
|
+
content-visibility: auto;
|
|
282
|
+
contain-intrinsic-size: 0 26px;
|
|
266
283
|
}
|
|
267
284
|
/* expanded: dash flips to a left-side indicator, content reads left-to-right */
|
|
268
285
|
.sr-rail[data-open="true"] .sr-turn {
|
|
@@ -848,7 +865,7 @@ function Rail(props) {
|
|
|
848
865
|
const row = findRow(turn.id)
|
|
849
866
|
const sc = scroller()
|
|
850
867
|
if (row && sc) {
|
|
851
|
-
|
|
868
|
+
alignAfterLand(row)
|
|
852
869
|
pulse(row)
|
|
853
870
|
return
|
|
854
871
|
}
|
|
@@ -861,7 +878,7 @@ function Rail(props) {
|
|
|
861
878
|
|
|
862
879
|
// Fast path: the row is mounted, scroll locally.
|
|
863
880
|
if (row && sc) {
|
|
864
|
-
|
|
881
|
+
alignAfterLand(row)
|
|
865
882
|
pulse(row)
|
|
866
883
|
flash("已定位")
|
|
867
884
|
return
|
|
@@ -934,11 +951,9 @@ function Rail(props) {
|
|
|
934
951
|
chainBase += chainPages
|
|
935
952
|
if (token !== jumpToken || sessionID() !== sessionAtJump) return
|
|
936
953
|
if (landed) {
|
|
937
|
-
|
|
938
|
-
if (live) scrollToRow(live, landed)
|
|
954
|
+
alignAfterLand(landed)
|
|
939
955
|
pulse(landed)
|
|
940
956
|
flash("已定位")
|
|
941
|
-
repairScroll(landed)
|
|
942
957
|
return
|
|
943
958
|
}
|
|
944
959
|
// The host went idle without landing: it exhausted this chain. The
|