opencode-session-turns 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gui-experimental.cjs +3 -3
- package/package.json +1 -1
- package/src/gui.tsx +66 -17
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("solid-js/web"),
|
|
2
|
-
`).map(
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("solid-js/web"),I=require("@opencode/plugin/gui"),i=require("solid-js");var Dt=a.template("<div class=sr-status>"),jt=a.template('<span class="sr-title sr-progress">跳转中'),Rt=a.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),qt=a.template("<span class=sr-title>会话轮次"),Bt=a.template("<div class=sr-note>"),Ot=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 Y="v13",Q="message-",vt="session-turns:diag",tt="session-turns:open",Jt=40,Nt=200,Ut=50,zt=120,O=45e3,Ft=3e3,Gt=300,Ht=220,Wt=800;let $t=0,et="",rt="";const yt=o=>{const r=String(o?.text??"").split(`
|
|
2
|
+
`).map(m=>m.trim()).find(m=>m.length>0);if(r)return r;const d=o?.files?.length??0;return d>0?`[附件 ×${d}]`:"(空消息)"},kt=o=>String(o).padStart(2,"0"),Vt=o=>{const r=new Date(o??0);return`${kt(r.getHours())}:${kt(r.getMinutes())}`},St=o=>new Promise(r=>setTimeout(r,o)),nt=()=>document.querySelector('[data-slot="session-timeline-scroll"] > .scroll-view__viewport')??document.querySelector('[data-slot="session-timeline-scroll"] [data-scrollable]')??(()=>{let r=document.querySelector("[data-message-id]")?.parentElement;for(;r;){const d=getComputedStyle(r);if(/(auto|scroll)/.test(d.overflowY)&&r.scrollHeight>r.clientHeight+1)return r;r=r.parentElement}})(),_=o=>document.getElementById(`${Q}${o}`)??[...document.querySelectorAll("[data-message-id]")].find(r=>r.getAttribute("data-message-id")===o),Z=o=>{const r=nt(),d=[...document.querySelectorAll("[data-message-id]")],m=d[0]?.getAttribute("data-message-id")??"",k=rt?` · 目标 ${_(rt)?"在":"不在"} DOM`:"",C=m?` · 行ID ${m.slice(0,20)}`:"";let v="(空)";try{v=o.router.path()||"(空)"}catch{}return`轮次 ${$t} · 容器 ${r?"OK":"缺失"} · DOM ${d.length} 行${k}${C} · 路由 ${v} · hash ${window.location.hash||"(空)"}${et?` · ${et}`:""}`},X=(o,r)=>{const d=o.querySelector("[data-session-title]"),m=o.getBoundingClientRect(),k=d instanceof HTMLElement?d.getBoundingClientRect().bottom:m.top,v=r.getBoundingClientRect().top-(k+24);Math.abs(v)>2&&(o.scrollTop+=v)},Kt=o=>{setTimeout(()=>{if(!o.isConnected)return;const r=nt();if(!r)return;const d=r.querySelector("[data-session-title]"),m=d instanceof HTMLElement?d.getBoundingClientRect().bottom:r.getBoundingClientRect().top,k=o.getBoundingClientRect().top;k<m+4&&(r.scrollTop+=k-(m+24))},300)},Yt=o=>{let r="";try{r=o.router.path()||""}catch{}return r.startsWith("/")?r:""},Zt=o=>{const r=document.createElement("a");r.setAttribute("href",o),r.setAttribute("replace",""),r.setAttribute("noscroll",""),r.style.display="none",document.body.appendChild(r);try{r.dispatchEvent(new MouseEvent("click",{bubbles:!0,cancelable:!0,button:0,view:window}))}finally{r.remove()}},Xt=`
|
|
3
3
|
.sr-rail {
|
|
4
4
|
position: fixed;
|
|
5
5
|
top: 50%;
|
|
@@ -215,4 +215,4 @@
|
|
|
215
215
|
white-space: pre-wrap;
|
|
216
216
|
max-width: min(720px, 90vw);
|
|
217
217
|
}
|
|
218
|
-
`;function
|
|
218
|
+
`;function Qt(o){const r=I.usePlugin(),[d,m]=i.createSignal([]),[k,C]=i.createSignal(""),[v,T]=i.createSignal(!1),[Et,Tt]=i.createSignal(""),[st,ot]=i.createSignal(""),[y,P]=i.createSignal(!1),[J,A]=i.createSignal(0),[N,D]=i.createSignal(""),S=new Map;let U=0,j=!1,R=!1,z="",L;const $=()=>o.session()?.id,F=()=>{const t=[...S.values()].sort((n,l)=>n.created-l.created),e=i.untrack(d);e.length===t.length&&e.every((n,l)=>n===t[l])||(m(t),$t=t.length)},at=t=>{const e=S.get(t.id);e&&e.created===t.time&&e.text===t.text||S.set(t.id,{id:t.id,created:t.time,text:t.text})},_t=async(t,e,n)=>{const l=n?{sessionID:t,type:"user",limit:e,cursor:n}:{sessionID:t,type:"user",limit:e,order:"desc"};return await r.client.message.list(l)},G=async(t,e)=>{const n=++U;j=!0;let l="";try{let s;const c=e==="full"?Jt:1,u=e==="full"?Nt:Ut;let p=e!=="full";for(let f=0;f<c;f++){let x;try{x=await _t(t,u,s)}catch(h){l=String(h?.message??h);break}if(n!==U)return;p||(S.clear(),p=!0);let g=0;for(const h of x?.data??[])S.has(h.id)||(at({id:h.id,time:h.time?.created??0,text:yt(h)}),g++);F();const b=x?.cursor?.next;if(!b||g===0)break;s=b}}finally{j=!1,z=t,et=l,Tt(S.size===0?l||"暂无用户消息":""),R&&(R=!1,it())}},it=()=>{L||(L=setTimeout(()=>{L=void 0;const t=i.untrack($);t&&G(t,"light")},300))},lt=t=>{if(!(!t||t!==i.untrack($))){if(j){R=!0;return}it()}},Ct=t=>{const e=t?.type;if(e==="message.updated"){const n=t?.data??{},l=n.info;l?.role==="user"&&l?.id&&(at({id:l.id,time:l.time?.created??0,text:yt(l)}),F()),lt(n.sessionID)}else if(e==="message.removed"){const n=t?.data??{};n.messageID&&S.delete(n.messageID)&&F(),lt(n.sessionID)}else if(e==="location.shutdown"){const n=i.untrack($);n&&z===n&&G(n,"full")}};i.onMount(()=>{let t=!1,e=0,n;(async()=>{for(;!t;){const c=r.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!t;){const u=await c.next();if(u.done)break;e=0,Ct(u.value)}}catch{}try{await c.return?.()}catch{}if(t)break;e++,await St(Math.min(2e3*e,15e3))}})(),i.onCleanup(()=>{t=!0,clearTimeout(n),L&&clearTimeout(L)})}),i.createEffect(()=>{const t=$();t&&i.untrack(()=>{z!==t&&(U++,j=!1,R=!1,S.clear(),m([]),C(""),W="",G(t,"full"))})});let q;const M=()=>(q?.isConnected||(q=nt()),q),At=()=>{const t=M();if(!t)return;const e=t.querySelectorAll("[data-message-id]");if(e.length===0)return;const n=t.getBoundingClientRect().top+40;for(const l of e)if(l.getBoundingClientRect().bottom>=n){const s=l.getAttribute("data-message-id");s&&C(s);break}};let H=!1;const ct=()=>{H||(H=!0,requestAnimationFrame(()=>{H=!1,At()}))};i.onMount(()=>{const t=()=>ct();document.addEventListener("scroll",t,!0);const e=setInterval(ct,1500);i.onCleanup(()=>{document.removeEventListener("scroll",t,!0),clearInterval(e)})});let dt=0;const w=(t,e=2600)=>{const n=++dt;ot(t),setTimeout(()=>{n===dt&&ot("")},e)};i.onMount(()=>{const t=n=>{if(n.key==="Escape"){if(i.untrack(y)){ht(),n.stopPropagation();return}i.untrack(v)&&(T(!1),n.stopPropagation())}},e=n=>{if(!i.untrack(v))return;(n.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||T(!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=()=>w(`${Y} · ${Z(r)}`),e=()=>T(n=>!n);window.addEventListener(vt,t),window.addEventListener(tt,e),i.onCleanup(()=>{window.removeEventListener(vt,t),window.removeEventListener(tt,e)}),w(`会话轮次 ${Y} 已加载`)});let W="",E=0,ut=0,pt="";const V=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{}},Lt=()=>{for(const t of d())if(_(t.id))return t},ft=(t,e)=>{Zt(`${t}#${Q}${e}`),W=e},gt=()=>{const e=M()?.querySelectorAll("[data-message-id]")??[];if(e.length===0)return"0";const n=e[0].getAttribute("data-message-id")??"",l=e[e.length-1].getAttribute("data-message-id")??"";return`${e.length}:${n}:${l}`},Mt=(t,e,n)=>new Promise(l=>{let s=!1,c,u,p;const f=Date.now(),x=gt();let g=x,b=f;const h=B=>{if(!s){s=!0;try{c?.disconnect()}catch{}clearInterval(u),clearTimeout(p),l(B??null)}},mt=B=>{if(e!==E)return h(null);const wt=B?_(t):document.getElementById(`${Q}${t}`);if(wt)return h(wt);if(!B)return;const xt=gt(),K=Date.now();if(xt!==g){g=xt,b=K;return}K-f>Gt&&g!==x&&K-b>350&&h(null)},bt=_(t);if(bt)return h(bt);c=new MutationObserver(()=>mt(!1)),c.observe(document.documentElement,{childList:!0,subtree:!0}),u=setInterval(()=>mt(!0),120),p=setTimeout(()=>h(_(t)),Math.max(400,n))}),ht=()=>{E++,P(!1),A(0),D(""),w("已取消跳转")},It=async t=>{if(y()&&N()===t.id){w(`正在跳转 · 第 ${J()} 页`,O);return}const e=++E;P(!1),A(0),D(""),rt=t.id;const n=Date.now();if(t.id===pt&&n-ut<Wt){const p=_(t.id),f=M();if(p&&f){X(f,p),V(p);return}}ut=n,pt=t.id;const l=M(),s=_(t.id);if(s&&l){X(l,s),V(s),w("已定位");return}const c=Yt(r);if(!c){w(`无法读取当前路由,无法定位 · ${Z(r)}`);return}const u=$();P(!0),D(t.id),A(0),T(!0),w("正在定位…",O);try{const p=Date.now()+O;for(let f=0;f<zt&&Date.now()<p;f++){if(e!==E||$()!==u)return;if(A(f+1),w(`跳转中 · 第 ${f+1} 页`,O),W===t.id){const b=Lt()??d()[0];b&&(ft(c,b.id),await St(Ht))}if(e!==E||$()!==u)return;ft(c,t.id);const x=Math.min(Ft,p-Date.now()),g=await Mt(t.id,e,x);if(e!==E||$()!==u)return;if(g){const b=M()??l;b&&X(b,g),V(g),w("已定位"),Kt(g);return}}e===E&&w(`未能定位 · ${Z(r)}`)}finally{e===E&&(P(!1),A(0),D(""))}};i.createEffect(()=>{y()&&N()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Pt=[a.createComponent(i.Show,{get when(){return st()},get children(){var t=Dt();return a.insert(t,st),t}}),(()=>{var t=Rt(),e=t.firstChild,n=e.firstChild,l=e.nextSibling;return t.addEventListener("mouseleave",()=>{y()||T(!1)}),t.addEventListener("mouseenter",()=>T(!0)),a.insert(e,a.createComponent(i.Show,{get when(){return y()},get fallback(){return(()=>{var s=qt();return s.firstChild,a.insert(s,(()=>{var c=a.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),s})()},get children(){var s=jt();return s.firstChild,a.insert(s,(()=>{var c=a.memo(()=>J()>0);return()=>c()?` · 第 ${J()} 页`:""})(),null),s}}),n),n.$$click=()=>{y()?ht():T(!1)},a.insert(l,a.createComponent(i.Show,{get when(){return d().length>0},get fallback(){return(()=>{var s=Bt();return a.insert(s,()=>Et()||`… ${Y}`),s})()},get children(){return a.createComponent(i.For,{get each(){return d()},children:(s,c)=>(()=>{var u=Ot(),p=u.firstChild,f=p.firstChild;f.firstChild;var x=f.nextSibling;return u.$$click=()=>{It(s)},a.insert(f,()=>c()+1,null),a.insert(x,()=>Vt(s.created)),a.insert(p,()=>s.text,null),a.effect(g=>{var b=k()===s.id?"true":"false",h=y()&&N()===s.id?"true":"false";return b!==g.e&&a.setAttribute(u,"data-active",g.e=b),h!==g.t&&a.setAttribute(u,"data-busy",g.t=h),g},{e:void 0,t:void 0}),u})()})}})),a.effect(s=>{var c=v()?"true":"false",u=y()?"true":"false",p=y()?"取消跳转 (Esc)":"关闭 (Esc)";return c!==s.e&&a.setAttribute(t,"data-open",s.e=c),u!==s.t&&a.setAttribute(t,"data-jumping",s.t=u),p!==s.a&&a.setAttribute(n,"title",s.a=p),s},{e:void 0,t:void 0,a:void 0}),t})()];return a.createComponent(a.Portal,{get mount(){return document.body},children:Pt})}const te=I.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(I.Style,Xt),o.add(I.Slot,{at:"window.bottom",render:()=>a.createComponent(i.Show,{get when(){return o.sessions.current()},get children(){return a.createComponent(Qt,{session:()=>o.sessions.current()})}})}),o.add(I.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(tt))}}))}});a.delegateEvents(["click"]);exports.default=te;
|
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 = "v13"
|
|
10
10
|
const HASH_PREFIX = "message-"
|
|
11
11
|
const DIAG_EVENT = "session-turns:diag"
|
|
12
12
|
const OPEN_EVENT = "session-turns:open"
|
|
@@ -18,12 +18,13 @@ const FULL_PAGE_SIZE = 200
|
|
|
18
18
|
const LIGHT_PAGE_SIZE = 50
|
|
19
19
|
|
|
20
20
|
// jump loop budgets. One round pages in roughly one host history page (~5
|
|
21
|
-
// turns); a round ends
|
|
22
|
-
//
|
|
21
|
+
// turns); a round ends once the host's row set has stopped changing, so a
|
|
22
|
+
// half-finished page is never interrupted by the next hash toggle.
|
|
23
23
|
const JUMP_ROUNDS = 120
|
|
24
24
|
const JUMP_TOTAL_MS = 45000
|
|
25
|
-
const JUMP_ROW_TIMEOUT_MS =
|
|
25
|
+
const JUMP_ROW_TIMEOUT_MS = 3000
|
|
26
26
|
const JUMP_SETTLE_MS = 300
|
|
27
|
+
const JUMP_TOGGLE_DWELL_MS = 220
|
|
27
28
|
const SAME_TARGET_WINDOW_MS = 800
|
|
28
29
|
|
|
29
30
|
// Filled by Rail.load()/jump() so the command palette can report diagnostics.
|
|
@@ -89,12 +90,33 @@ const diagnostics = (ctx) => {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
const scrollToRow = (scroller, row) => {
|
|
92
|
-
//
|
|
93
|
-
// the
|
|
94
|
-
|
|
93
|
+
// Align by rects instead of scrollIntoView: the host puts the row top flush
|
|
94
|
+
// against the sticky session title, but a 16px gradient fade sits right below
|
|
95
|
+
// it and would cover the top of the bubble (and our outline). Leave 24px so
|
|
96
|
+
// the bubble clears both the title and the fade.
|
|
95
97
|
const title = scroller.querySelector("[data-session-title]")
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
+
const scRect = scroller.getBoundingClientRect()
|
|
99
|
+
const titleBottom =
|
|
100
|
+
title instanceof HTMLElement ? title.getBoundingClientRect().bottom : scRect.top
|
|
101
|
+
const rowTop = row.getBoundingClientRect().top
|
|
102
|
+
const delta = rowTop - (titleBottom + 24)
|
|
103
|
+
if (Math.abs(delta) > 2) scroller.scrollTop += delta
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The host's own hash seek may finish after ours and re-align the row flush
|
|
107
|
+
// under the sticky title (its 16px fade then covers the bubble's top edge).
|
|
108
|
+
// Repair only that case: if the user already scrolled elsewhere, leave it be.
|
|
109
|
+
const repairScroll = (row) => {
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
if (!row.isConnected) return
|
|
112
|
+
const sc = findScroller()
|
|
113
|
+
if (!sc) return
|
|
114
|
+
const title = sc.querySelector("[data-session-title]")
|
|
115
|
+
const titleBottom =
|
|
116
|
+
title instanceof HTMLElement ? title.getBoundingClientRect().bottom : sc.getBoundingClientRect().top
|
|
117
|
+
const rowTop = row.getBoundingClientRect().top
|
|
118
|
+
if (rowTop < titleBottom + 4) sc.scrollTop += rowTop - (titleBottom + 24)
|
|
119
|
+
}, 300)
|
|
98
120
|
}
|
|
99
121
|
|
|
100
122
|
// The desktop app (2.2.6) routes through a memory history, so window.location is
|
|
@@ -733,6 +755,8 @@ function Rail(props) {
|
|
|
733
755
|
let timer
|
|
734
756
|
const started = Date.now()
|
|
735
757
|
const baseline = rowSignature()
|
|
758
|
+
let lastSig = baseline
|
|
759
|
+
let stableSince = started
|
|
736
760
|
const finish = (row) => {
|
|
737
761
|
if (done) return
|
|
738
762
|
done = true
|
|
@@ -745,12 +769,24 @@ function Rail(props) {
|
|
|
745
769
|
}
|
|
746
770
|
const check = (full) => {
|
|
747
771
|
if (token !== jumpToken) return finish(null)
|
|
748
|
-
|
|
772
|
+
// Observer callbacks use the cheap id lookup; only the poll pays for the
|
|
773
|
+
// data-attribute fallback (a full scan) and the row fingerprint.
|
|
774
|
+
const row = full ? findRow(messageID) : document.getElementById(`${HASH_PREFIX}${messageID}`)
|
|
749
775
|
if (row) return finish(row)
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (
|
|
776
|
+
if (!full) return
|
|
777
|
+
const sig = rowSignature()
|
|
778
|
+
const now = Date.now()
|
|
779
|
+
if (sig !== lastSig) {
|
|
780
|
+
lastSig = sig
|
|
781
|
+
stableSince = now
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
// Only treat the page as done once the host has stopped appending rows
|
|
785
|
+
// for a moment: toggling the hash mid-load cancels its paging and the
|
|
786
|
+
// jump stalls until the user clicks again.
|
|
787
|
+
if (now - started > JUMP_SETTLE_MS && lastSig !== baseline && now - stableSince > 350) {
|
|
788
|
+
finish(null)
|
|
789
|
+
}
|
|
754
790
|
}
|
|
755
791
|
const existing = findRow(messageID)
|
|
756
792
|
if (existing) return finish(existing)
|
|
@@ -769,6 +805,15 @@ function Rail(props) {
|
|
|
769
805
|
}
|
|
770
806
|
|
|
771
807
|
const jump = async (turn) => {
|
|
808
|
+
// Clicking the same turn again while its jump is running must NOT restart
|
|
809
|
+
// the loop: every restart cancels the host's in-flight page load, which is
|
|
810
|
+
// what makes a deep jump advance only one page per click. Re-affirm the
|
|
811
|
+
// progress instead and let the running loop finish.
|
|
812
|
+
if (jumping() && busyID() === turn.id) {
|
|
813
|
+
flash(`正在跳转 · 第 ${jumpPage()} 页`, JUMP_TOTAL_MS)
|
|
814
|
+
return
|
|
815
|
+
}
|
|
816
|
+
|
|
772
817
|
const token = ++jumpToken
|
|
773
818
|
// A new click always owns the progress UI. This also clears a previous
|
|
774
819
|
// jump's state when the new target takes the mounted fast path, where the
|
|
@@ -836,20 +881,24 @@ function Rail(props) {
|
|
|
836
881
|
const other = oldestLoadedTurn() ?? turns()[0]
|
|
837
882
|
if (other) {
|
|
838
883
|
navTo(path, other.id)
|
|
839
|
-
|
|
884
|
+
// Let the host commit the intermediate hash before seeking back;
|
|
885
|
+
// too fast and both clicks collapse into the same target hash, so
|
|
886
|
+
// the cursor machine never runs another page.
|
|
887
|
+
await sleep(JUMP_TOGGLE_DWELL_MS)
|
|
840
888
|
}
|
|
841
889
|
}
|
|
842
|
-
if (token !== jumpToken) return
|
|
890
|
+
if (token !== jumpToken || sessionID() !== sessionAtJump) return
|
|
843
891
|
navTo(path, turn.id)
|
|
844
892
|
|
|
845
893
|
const remaining = Math.min(JUMP_ROW_TIMEOUT_MS, deadline - Date.now())
|
|
846
894
|
const landed = await waitForRow(turn.id, token, remaining)
|
|
847
|
-
if (token !== jumpToken) return
|
|
895
|
+
if (token !== jumpToken || sessionID() !== sessionAtJump) return
|
|
848
896
|
if (landed) {
|
|
849
897
|
const live = scroller() ?? sc
|
|
850
898
|
if (live) scrollToRow(live, landed)
|
|
851
899
|
pulse(landed)
|
|
852
900
|
flash("已定位")
|
|
901
|
+
repairScroll(landed)
|
|
853
902
|
return
|
|
854
903
|
}
|
|
855
904
|
}
|