opencode-session-turns 0.1.16 → 0.1.17
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 +16 -1
- package/dist/gui-experimental.cjs +76 -3
- package/package.json +2 -2
- package/src/gui.tsx +321 -34
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@ click a turn to jump straight to that message.
|
|
|
21
21
|
|
|
22
22
|
- 列出当前会话的每一轮用户提问(从会话消息接口分页拉取,`type=user`)。
|
|
23
23
|
- 鼠标移到窗口右缘的细条上自动展开,移开自动收起;长历史跳转期间面板保持展开。
|
|
24
|
+
- **搜索轮次**:≥ 8 轮时面板顶部出现搜索框,按关键字即时过滤(匹配数显示在标题里);`↑`/`↓` 在结果间移动光标,`Enter` 跳转,无鼠标也能导航。
|
|
25
|
+
- **面板固定**:标题栏图钉按钮或 `⌘⌥P`(斜杠命令 `/turns-pin`)把面板钉住,鼠标移开不收起;`Esc` 先解固定、再关面板。
|
|
24
26
|
- 点击任意一轮,**跳转到对应消息**——包括很久以前、尚未渲染的历史轮次。
|
|
25
27
|
- 深历史需要逐页加载时,面板头部显示进度,目标轮次带徽标并琥珀色脉冲;
|
|
26
28
|
按 `Esc` 或点 × 可随时取消。
|
|
@@ -30,7 +32,9 @@ click a turn to jump straight to that message.
|
|
|
30
32
|
- **点击与预热的配合**:点到的轮次已预热 → 直接瞬时跳转;还没预热到 → 头部显示
|
|
31
33
|
「预热中 · 稍等一下」,等预热翻到该轮后自动落地(不打断预热)。
|
|
32
34
|
- 定位成功后精准描边闪烁**那一条用户气泡**(跟随气泡自身圆角),不再整行填色。
|
|
33
|
-
-
|
|
35
|
+
- 自动高亮当前视口所在的轮次(IntersectionObserver 驱动,后台窗口零开销)。
|
|
36
|
+
- 跨天会话的时间标注会带上「昨天」或日期,不再只有 `HH:mm` 造成歧义。
|
|
37
|
+
- 尊重系统的「减弱动态效果」(`prefers-reduced-motion`):保留状态色彩,去掉动画。
|
|
34
38
|
- 斜杠命令 `/turns`、快捷键 `⌘⌥R`(Windows/Linux 为 `Ctrl+Alt+R`)展开轮次条。
|
|
35
39
|
|
|
36
40
|
## 安装
|
|
@@ -88,6 +92,17 @@ npm run build # -> dist/gui-experimental.cjs
|
|
|
88
92
|
改完源码后重新构建,并在 OpenCode Desktop 里 Reload 插件
|
|
89
93
|
(Settings → Extensions → Plugins → Reload)或重启应用。
|
|
90
94
|
|
|
95
|
+
## 发布
|
|
96
|
+
|
|
97
|
+
版本发布只通过 GitHub Actions(`.github/workflows/publish.yml`);本地 `npm publish` 已被 `prepublishOnly` 守卫禁止:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
npm version patch # 或 minor / major:自动改版本、提交并打 v* tag
|
|
101
|
+
git push --follow-tags origin main
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
CI 会校验 tag 与 `package.json` 版本一致,随后构建并发布到 npm。
|
|
105
|
+
|
|
91
106
|
## 许可证
|
|
92
107
|
|
|
93
108
|
MIT © meiyx7
|
|
@@ -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 r=require("solid-js/web"),F=require("@opencode/plugin/gui"),a=require("solid-js");var tt=r.template("<div class=sr-status>"),xt=r.template('<span class="sr-title sr-progress">'),yt=r.template('<svg><path d="M6 3l7 7-3.5.5L7 13 3 9l2.5-2.5z"></svg>',!1,!0,!1),kt=r.template('<svg><path d="M4 12l-1.5 1.5"></svg>',!1,!0,!1),$t=r.template("<button type=button class=sr-search-clear aria-label=清除搜索>×"),St=r.template("<div class=sr-search><input type=text class=sr-search-input placeholder=搜索轮次…>"),Ct=r.template("<div class=sr-foot>"),Et=r.template('<div class=sr-rail><div class=sr-head><span class=sr-head-actions><button type=button class=sr-iconbtn><svg viewBox="0 0 16 16"fill=none stroke=currentColor stroke-width=1.5 stroke-linecap=round stroke-linejoin=round aria-hidden=true></svg></button><button type=button class=sr-close>×</button></span></div><div class=sr-list tabindex=-1>'),_t=r.template("<span class=sr-title>会话轮次"),At=r.template('<svg><path d="M9.5 2.5l4 4-2.6.9-1.4 3.5-4.4-4.4L8.6 5.1z"></svg>',!1,!0,!1),Mt=r.template('<svg><path d="M5.1 6.5L2.5 9.1l4.4 4.4 2.6-2.6"></svg>',!1,!0,!1),Tt=r.template("<div class=sr-note>"),Dt=r.template("<span class=sr-badge>"),It=r.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 rt="v21",Se="message-",nt="session-turns:diag",Ce="session-turns:open",Ee="session-turns:pin",Lt=40,Pt=200,jt=50,qt=30,Rt=45e3,Bt=2e4,Ot=8e3,zt=12e3,st=220,Ft=800;let at=0,_e="",Ae="",Me="";const ot=l=>{const s=String(l?.text??"").split(`
|
|
2
|
+
`).map(m=>m.trim()).find(m=>m.length>0);if(s)return s;const c=l?.files?.length??0;return c>0?`[附件 ×${c}]`:"(空消息)"},P=l=>String(l).padStart(2,"0"),Jt=l=>{const s=new Date(l??0),c=new Date;if(s.getFullYear()===c.getFullYear()&&s.getMonth()===c.getMonth()&&s.getDate()===c.getDate())return`${P(s.getHours())}:${P(s.getMinutes())}`;const $=new Date(c);return $.setDate(c.getDate()-1),s.getFullYear()===$.getFullYear()&&s.getMonth()===$.getMonth()&&s.getDate()===$.getDate()?`昨天 ${P(s.getHours())}:${P(s.getMinutes())}`:`${P(s.getMonth()+1)}-${P(s.getDate())} ${P(s.getHours())}:${P(s.getMinutes())}`},de=l=>new Promise(s=>setTimeout(s,l)),Te=()=>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 c=getComputedStyle(s);if(/(auto|scroll)/.test(c.overflowY)&&s.scrollHeight>s.clientHeight+1)return s;s=s.parentElement}})(),E=l=>{const s=document.getElementById(`${Se}${l}`);if(s)return s;const c=`[data-message-id="${l.replace(/"/g,'\\"')}"]`;for(const m of document.querySelectorAll(c))if(m.getAttribute("data-timeline-row")==="UserMessage"||m.querySelector('[data-slot="user-message-text"]'))return m},ke=l=>{const s=Te(),c=[...document.querySelectorAll("[data-message-id]")],m=c[0]?.getAttribute("data-message-id")??"",$=Me?` · 目标 ${E(Me)?"在":"不在"} DOM`:"",T=m?` · 行ID ${m.slice(0,20)}`:"";let S="(空)";try{S=l.router.path()||"(空)"}catch{}return`轮次 ${at} · 容器 ${s?"OK":"缺失"} · DOM ${c.length} 行${$}${T} · 路由 ${S} · hash ${window.location.hash||"(空)"}${Ae?` · ${Ae}`:""}${_e?` · ${_e}`:""}`},Nt=l=>{const s=l.getBoundingClientRect().top;let c=s;for(const m of l.querySelectorAll(".sticky, [data-session-title]")){if(!(m instanceof HTMLElement)||getComputedStyle(m).position!=="sticky"&&!m.hasAttribute("data-session-title"))continue;const T=m.getBoundingClientRect();T.height!==0&&(T.top>s+4||(c=Math.max(c,T.bottom)))}return c},$e=l=>{try{const s=Te();if(!s||!l.isConnected)return;const c=l.querySelector('[data-slot="user-message-text"]')??l,m=Nt(s),$=c.getBoundingClientRect().top-(m+18);Math.abs($)>2&&(s.scrollTop+=$),Ae=`inset ${Math.round(m)} · delta ${Math.round($)}`}catch{}},ue=l=>{$e(l),setTimeout(()=>$e(l),450),setTimeout(()=>$e(l),1e3)},it=l=>{let s="";try{s=l.router.path()||""}catch{}return s.startsWith("/")?s:""},Ht=l=>{const s=document.createElement("a");s.setAttribute("href",l),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()}},Ut=`
|
|
3
3
|
.sr-rail {
|
|
4
4
|
position: fixed;
|
|
5
5
|
top: 50%;
|
|
@@ -46,6 +46,73 @@
|
|
|
46
46
|
color: rgba(255,170,60,.95);
|
|
47
47
|
font-variant-numeric: tabular-nums;
|
|
48
48
|
}
|
|
49
|
+
.sr-head-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
|
|
50
|
+
.sr-iconbtn {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
width: 22px;
|
|
55
|
+
height: 22px;
|
|
56
|
+
padding: 0;
|
|
57
|
+
border: 0;
|
|
58
|
+
background: transparent;
|
|
59
|
+
color: inherit;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
opacity: .5;
|
|
63
|
+
}
|
|
64
|
+
.sr-iconbtn svg { width: 13px; height: 13px; }
|
|
65
|
+
.sr-iconbtn:hover { opacity: 1; background: var(--surface-base-hover, rgba(127,127,127,.14)); }
|
|
66
|
+
.sr-iconbtn[data-on="true"] { opacity: .95; color: rgba(255,170,60,.95); }
|
|
67
|
+
.sr-iconbtn:focus-visible { outline: 1px solid currentColor; outline-offset: -1px; }
|
|
68
|
+
/* search box */
|
|
69
|
+
.sr-search {
|
|
70
|
+
position: relative;
|
|
71
|
+
flex-shrink: 0;
|
|
72
|
+
margin: 0 0 6px;
|
|
73
|
+
}
|
|
74
|
+
.sr-search-input {
|
|
75
|
+
width: 100%;
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
height: 26px;
|
|
78
|
+
padding: 0 24px 0 8px;
|
|
79
|
+
border: 1px solid rgba(127,127,127,.25);
|
|
80
|
+
border-radius: 6px;
|
|
81
|
+
background: color-mix(in srgb, currentColor 6%, transparent);
|
|
82
|
+
color: inherit;
|
|
83
|
+
font: inherit;
|
|
84
|
+
font-size: 12px;
|
|
85
|
+
outline: none;
|
|
86
|
+
transition: border-color .12s ease, box-shadow .12s ease;
|
|
87
|
+
}
|
|
88
|
+
.sr-search-input::placeholder { color: currentColor; opacity: .4; }
|
|
89
|
+
.sr-search-input:focus {
|
|
90
|
+
border-color: rgba(255,170,60,.6);
|
|
91
|
+
box-shadow: 0 0 0 2px rgba(255,170,60,.14);
|
|
92
|
+
}
|
|
93
|
+
.sr-search-clear {
|
|
94
|
+
position: absolute;
|
|
95
|
+
right: 4px;
|
|
96
|
+
top: 50%;
|
|
97
|
+
transform: translateY(-50%);
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
width: 16px;
|
|
102
|
+
height: 16px;
|
|
103
|
+
padding: 0;
|
|
104
|
+
border: 0;
|
|
105
|
+
border-radius: 50%;
|
|
106
|
+
background: transparent;
|
|
107
|
+
color: inherit;
|
|
108
|
+
font-size: 12px;
|
|
109
|
+
line-height: 1;
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
opacity: .5;
|
|
112
|
+
}
|
|
113
|
+
.sr-search-clear:hover { opacity: 1; background: var(--surface-base-hover, rgba(127,127,127,.14)); }
|
|
114
|
+
/* keyboard cursor (↑/↓) */
|
|
115
|
+
.sr-turn[data-cursor="true"] { background: var(--surface-base-hover, rgba(127,127,127,.14)); }
|
|
49
116
|
.sr-close {
|
|
50
117
|
border: 0;
|
|
51
118
|
background: transparent;
|
|
@@ -242,4 +309,10 @@
|
|
|
242
309
|
white-space: pre-wrap;
|
|
243
310
|
max-width: 260px;
|
|
244
311
|
}
|
|
245
|
-
`;function St(l){const o=F.usePlugin(),[d,f]=a.createSignal([]),[C,T]=a.createSignal(""),[A,M]=a.createSignal(!1),[Ge,Ve]=a.createSignal(""),[j,xe]=a.createSignal(""),[E,H]=a.createSignal(!1),[ve,R]=a.createSignal(0),[ye,q]=a.createSignal(0),[Et,B]=a.createSignal(0),[U,W]=a.createSignal(""),[ke,J]=a.createSignal(!1),_=new Map;let se=0,O=!1,G=!1,V="",N;const h=()=>l.session()?.id,oe=()=>{const e=[..._.values()].sort((r,i)=>r.created-i.created),t=a.untrack(d);t.length===e.length&&t.every((r,i)=>r===e[i])||(f(e),We=e.length)},Se=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})},Ye=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)},ie=async(e,t)=>{const r=++se;O=!0,t==="full"&&(z="",de="");let i="";try{let n;const c=t==="full"?dt:1,u=t==="full"?ut:pt;let m=t!=="full";for(let x=0;x<c;x++){let v;try{v=await Ye(e,u,n)}catch(g){i=String(g?.message??g);break}if(r!==se)return;m||(_.clear(),m=!0);let b=0;for(const g of v?.data??[])_.has(g.id)||(Se({id:g.id,time:g.time?.created??0,text:Fe(g)}),b++);oe();const p=v?.cursor?.next;if(!p||b===0)break;n=p}}finally{O=!1,V=e,he=i,Ve(_.size===0?i||"暂无用户消息":""),G&&(G=!1,$e()),t==="full"&&!i&&e===a.untrack(h)&&_.size>=15&&setTimeout(()=>ee(),1200)}},$e=()=>{N||(N=setTimeout(()=>{N=void 0;const e=a.untrack(h);e&&ie(e,"light")},300))},Ee=e=>{if(!(!e||e!==a.untrack(h))){if(O){G=!0;return}$e()}},Ke=e=>{const t=e?.type;if(t==="message.updated"){const r=e?.data??{},i=r.info;i?.role==="user"&&i?.id&&(Se({id:i.id,time:i.time?.created??0,text:Fe(i)}),oe()),Ee(r.sessionID)}else if(t==="message.removed"){const r=e?.data??{};r.messageID&&_.delete(r.messageID)&&oe(),Ee(r.sessionID)}else if(t==="location.shutdown"){const r=a.untrack(h);r&&V===r&&ie(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,Ke(u.value)}}catch{}try{await c.return?.()}catch{}if(e)break;t++,await re(Math.min(2e3*t,15e3))}})(),a.onCleanup(()=>{e=!0,clearTimeout(r),N&&clearTimeout(N)})}),a.createEffect(()=>{const e=h();e&&a.untrack(()=>{V!==e&&(se++,O=!1,G=!1,_.clear(),f([]),T(""),le="",ie(e,"full"))})});let Y;const I=()=>(Y?.isConnected||(Y=we()),Y),Ze=()=>{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&&T(n);break}};let ae=!1;const _e=()=>{ae||(ae=!0,requestAnimationFrame(()=>{ae=!1,Ze()}))};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 Ce=0;const K=(e,t=2600)=>{const r=++Ce;xe(e),setTimeout(()=>{r===Ce&&xe("")},t)};a.onMount(()=>{const e=r=>{if(r.key==="Escape"){if(a.untrack(E)){Le(),r.stopPropagation();return}a.untrack(A)&&(M(!1),r.stopPropagation())}},t=r=>{if(!a.untrack(A))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(`${Oe} · ${ue(o)}`),t=()=>{M(r=>!r),ee()};window.addEventListener(Ne,e),window.addEventListener(ge,t),a.onCleanup(()=>{window.removeEventListener(Ne,e),window.removeEventListener(ge,t)})});let le="",S=0,Te=0,Ae="";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{}},Me=()=>{for(const e of d())if($(e.id))return e},L=(e,t)=>{yt(`${e}#${fe}${t}`),le=t},X=()=>{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}`},Ie=(e,t,r,i)=>new Promise(n=>{let c=!1,u,m,x;const v=Date.now();let p=X(),g=v,w=0;const y=te=>{if(!c){c=!0;try{u?.disconnect()}catch{}clearInterval(m),clearTimeout(x),n(te??null)}},k=te=>{if(t!==S)return y(null);const Re=te?$(e):document.getElementById(`${fe}${e}`);if(Re)return y(Re);if(!te)return;const qe=X(),Be=Date.now();if(qe!==p){p=qe,g=Be,w++,i?.(w);return}const rt=w>0?mt:bt;Be-g>rt&&y(null)},je=$(e);if(je)return y(je);u=new MutationObserver(()=>k(!1)),u.observe(document.documentElement,{childList:!0,subtree:!0}),m=setInterval(()=>k(!0),120),x=setTimeout(()=>y($(e)),Math.max(400,r))}),Le=()=>{S++,H(!1),R(0),q(0),B(0),W(""),J(!1),K("已取消跳转")},Xe=async e=>{if(E()&&U()===e.id)return;const t=Ue(o),r=I(),i=$(e.id);if(i&&r){D()&&(Q(),t&&L(t,e.id)),ne(i),Z(i);return}const n=Date.now();if(e.id===Ae&&n-Te<wt){const b=$(e.id);if(b&&r){ne(b),Z(b);return}}if(Te=n,Ae=e.id,be=e.id,!t){K(`无法读取当前路由,无法定位 · ${ue(o)}`);return}const c=++S,u=h();H(!0),W(e.id),R(0),q(0),B(0),J(D()),M(!0);const m=Date.now(),x=setInterval(()=>{q(Math.round((Date.now()-m)/1e3))},500);let v=0;try{if(D()){const p=await Ie(e.id,c,2e4,g=>R(g));if(c!==S||h()!==u)return;if(p){Q(),L(t,e.id),ne(p),Z(p);return}if(D()&&Q(),J(!1),c!==S||h()!==u)return}const b=Date.now()+gt;for(let p=0;p<ft&&Date.now()<b;p++){if(c!==S||h()!==u)return;if(B(p+1),le===e.id){const k=Me()??d()[0];k&&(L(t,k.id),await re(ze))}if(c!==S||h()!==u)return;L(t,e.id);let g=0;const w=Math.min(ht,b-Date.now()),y=await Ie(e.id,c,w,k=>{g=k,R(v+k)});if(v+=g,c!==S||h()!==u)return;if(y){ne(y),Z(y);return}}c===S&&K(`未能定位 · ${ue(o)}`)}finally{clearInterval(x),c===S&&(H(!1),R(0),q(0),B(0),W(""),J(!1))}},[D,ce]=a.createSignal(!1);let P=0,z="",de="";const Qe=()=>{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}}},De=e=>{if(!e)return;const t=I();if(!t)return;const r=e.el?.isConnected?e.el:$(e.id);if(!r)return;const i=t.getBoundingClientRect(),n=r.getBoundingClientRect().top-i.top-e.offset;Math.abs(n)>2&&(t.scrollTop+=n)},Q=()=>{P++,ce(!1)},ee=()=>{const e=h();!e||D()||z!==e&&(d().length<15||et())},et=async()=>{if(D())return;const e=Ue(o),t=d()[0],r=h();if(!e||!t||!r||z===r&&$(t.id))return;const i=++P,n=Qe(),c=I(),u=c?c.scrollTop+c.clientHeight>=c.scrollHeight-32:!0;ce(!0);try{L(e,t.id);const m=Date.now()+6e4;let x=X(),v=Date.now(),b=0;const p=setInterval(()=>De(n),150);try{for(;Date.now()<m&&i===P&&!($(t.id)||(await re(250),i!==P));){const w=Date.now(),y=X();if(y!==x)x=y,v=w,b=0;else if(w-v>5e3){if(b>=2)break;const k=Me();if(k&&k.id!==t.id&&(L(e,k.id),await re(ze),i!==P))break;L(e,t.id),v=Date.now(),b++}}}finally{clearInterval(p)}if(i!==P)return;const g=!!$(t.id);if(De(n),u){const w=I();w&&(w.scrollTop=w.scrollHeight)}g?z=r:de=r}finally{i===P&&ce(!1)}};let Pe=a.untrack(h);a.createEffect(()=>{const e=h();e!==Pe&&(Pe=e,Q(),S++,H(!1),R(0),q(0),B(0),W(""),J(!1))}),a.createEffect(()=>{const e=h(),t=d().length,r=D();if(!e||t<15||r||z===e||de===e||O||V!==e)return;const i=setTimeout(()=>ee(),1200);a.onCleanup(()=>clearTimeout(i))}),a.createEffect(()=>{E()&&U()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const tt=[s.createComponent(a.Show,{get when(){return j()},get children(){var e=Je();return s.insert(e,j),e}}),(()=>{var e=ot(),t=e.firstChild,r=t.firstChild,i=t.nextSibling;return e.addEventListener("mouseleave",()=>{E()||M(!1)}),e.addEventListener("mouseenter",()=>{M(!0),ee()}),s.insert(t,s.createComponent(a.Show,{get when(){return E()},get fallback(){return(()=>{var n=it();return n.firstChild,s.insert(n,(()=>{var c=s.memo(()=>d().length>0);return()=>c()?` · ${d().length}`:""})(),null),n})()},get children(){var n=nt();return s.insert(n,()=>ke()?"预热中 · 稍等一下":"加载历史中",null),s.insert(n,(()=>{var c=s.memo(()=>ve()>0);return()=>c()?` · ${ve()} 页`:""})(),null),s.insert(n,(()=>{var c=s.memo(()=>ye()>0);return()=>c()?` · ${ye()}s`:""})(),null),n}}),r),r.$$click=()=>{E()?Le():M(!1)},s.insert(i,s.createComponent(a.Show,{get when(){return d().length>0},get fallback(){return(()=>{var n=at();return s.insert(n,()=>Ge()||`… ${Oe}`),n})()},get children(){return s.createComponent(a.For,{get each(){return d()},children:(n,c)=>(()=>{var u=ct(),m=u.firstChild,x=m.firstChild;x.firstChild;var v=x.nextSibling,b=m.nextSibling;return u.$$click=()=>{Xe(n)},s.insert(x,()=>c()+1,null),s.insert(v,()=>xt(n.created)),s.insert(m,()=>n.text,null),s.insert(u,s.createComponent(a.Show,{get when(){return s.memo(()=>!!E())()&&U()===n.id},get children(){var p=lt();return s.insert(p,()=>ke()?"稍等":"加载中"),p}}),b),s.effect(p=>{var g=C()===n.id?"true":"false",w=E()&&U()===n.id?"true":"false";return g!==p.e&&s.setAttribute(u,"data-active",p.e=g),w!==p.t&&s.setAttribute(u,"data-busy",p.t=w),p},{e:void 0,t:void 0}),u})()})}})),s.insert(e,s.createComponent(a.Show,{get when(){return s.memo(()=>!!A())()&&j()},get children(){var n=st();return s.insert(n,j),n}}),null),s.effect(n=>{var c=A()?"true":"false",u=E()?"true":"false",m=E()?"取消跳转 (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(()=>!A())()&&j()},get children(){var e=Je();return s.insert(e,j),e}})];return s.createComponent(s.Portal,{get mount(){return document.body},children:tt})}const $t=F.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(F.Style,kt),l.add(F.Slot,{at:"window.bottom",render:()=>s.createComponent(a.Show,{get when(){return l.sessions.current()},get children(){return s.createComponent(St,{session:()=>l.sessions.current()})}})}),l.add(F.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(ge))}}))}});s.delegateEvents(["click"]);exports.default=$t;
|
|
312
|
+
/* Reduced motion: keep the signals (colors, widths) but drop the animation. */
|
|
313
|
+
@media (prefers-reduced-motion: reduce) {
|
|
314
|
+
.sr-rail, .sr-dash, .sr-text { transition: none !important; }
|
|
315
|
+
.sr-turn[data-busy="true"] .sr-dash { animation: none !important; opacity: 1 !important; }
|
|
316
|
+
.sr-flash-ring { animation: none !important; box-shadow: inset 0 0 0 2px rgba(255,170,60,.5) !important; }
|
|
317
|
+
}
|
|
318
|
+
`;function Wt(l){const s=F.usePlugin(),[c,m]=a.createSignal([]),[$,T]=a.createSignal(""),[S,I]=a.createSignal(!1),[lt,ct]=a.createSignal(""),[O,De]=a.createSignal(""),[Q,Ie]=a.createSignal(""),[_,pe]=a.createSignal(!1),[A,X]=a.createSignal(!1),[Le,z]=a.createSignal(0),[Pe,J]=a.createSignal(0),[Gt,N]=a.createSignal(0),[ee,te]=a.createSignal(""),[je,H]=a.createSignal(!1),D=new Map;let fe=0,U=!1,re=!1,ne="",W;const k=()=>l.session()?.id,ge=()=>{const e=[...D.values()].sort((n,i)=>n.created-i.created),t=a.untrack(c);t.length===e.length&&t.every((n,i)=>n===e[i])||(m(e),at=e.length)},qe=e=>{const t=D.get(e.id);t&&t.created===e.time&&t.text===e.text||D.set(e.id,{id:e.id,created:e.time,text:e.text})},dt=async(e,t,n)=>{const i=n?{sessionID:e,type:"user",limit:t,cursor:n}:{sessionID:e,type:"user",limit:t,order:"desc"};return await s.client.message.list(i)},he=async(e,t)=>{const n=++fe;U=!0,t==="full"&&(G="",xe="");let i="";try{let g;const d=t==="full"?Lt:1,v=t==="full"?Pt:jt;let o=t!=="full";for(let u=0;u<d;u++){let f;try{f=await dt(e,v,g)}catch(w){i=String(w?.message??w);break}if(n!==fe)return;o||(D.clear(),o=!0);let h=0;for(const w of f?.data??[])D.has(w.id)||(qe({id:w.id,time:w.time?.created??0,text:ot(w)}),h++);ge();const b=f?.cursor?.next;if(!b||h===0)break;g=b}}finally{U=!1,ne=e,_e=i,ct(D.size===0?i||"暂无用户消息":""),re&&(re=!1,Re()),t==="full"&&!i&&e===a.untrack(k)&&D.size>=15&&setTimeout(()=>V(),1200)}},Re=()=>{W||(W=setTimeout(()=>{W=void 0;const e=a.untrack(k);e&&he(e,"light")},300))},Be=e=>{if(!(!e||e!==a.untrack(k))){if(U){re=!0;return}Re()}},ut=e=>{const t=e?.type;if(t==="message.updated"){const n=e?.data??{},i=n.info;i?.role==="user"&&i?.id&&(qe({id:i.id,time:i.time?.created??0,text:ot(i)}),ge()),Be(n.sessionID)}else if(t==="message.removed"){const n=e?.data??{};n.messageID&&D.delete(n.messageID)&&ge(),Be(n.sessionID)}else if(t==="location.shutdown"){const n=a.untrack(k);n&&ne===n&&he(n,"full")}};a.onMount(()=>{let e=!1,t=0,n;(async()=>{for(;!e;){const d=s.client.event.subscribe()[Symbol.asyncIterator]();try{for(;!e;){const v=await d.next();if(v.done)break;t=0,ut(v.value)}}catch{}try{await d.return?.()}catch{}if(e)break;t++,await de(Math.min(2e3*t,15e3))}})(),a.onCleanup(()=>{e=!0,clearTimeout(n),W&&clearTimeout(W)})}),a.createEffect(()=>{const e=k();e&&a.untrack(()=>{ne!==e&&(fe++,U=!1,re=!1,D.clear(),m([]),T(""),we="",he(e,"full"))})});let se;const j=()=>(se?.isConnected||(se=Te()),se),Oe=new WeakSet;let Y;const pt=()=>Y||(Y=new IntersectionObserver(e=>{e.length!==0&&be()},{rootMargin:"-40px 0px 0px 0px",threshold:0}),Y),ft=()=>{const e=pt();for(const t of document.querySelectorAll("[data-message-id]"))Oe.has(t)||(Oe.add(t),e.observe(t))},gt=()=>{const e=j();if(!e)return;const t=e.querySelectorAll("[data-message-id]");if(t.length===0)return;const n=e.getBoundingClientRect().top+40;for(const i of t)if(i.getBoundingClientRect().bottom>=n){const g=i.getAttribute("data-message-id");g&&g!==$()&&T(g);break}};let me=!1;const be=()=>{me||(me=!0,requestAnimationFrame(()=>{me=!1,gt(),ft()}))};a.onMount(()=>{const e=()=>be();document.addEventListener("scroll",e,!0);const t=setInterval(()=>{document.hidden||be()},4e3);a.onCleanup(()=>{document.removeEventListener("scroll",e,!0),clearInterval(t),Y?.disconnect()})});let ze=0;const oe=(e,t=2600)=>{const n=++ze;De(e),setTimeout(()=>{n===ze&&De("")},t)};a.onMount(()=>{const e=n=>{if(n.key==="Escape"){if(a.untrack(A)){Ue(),n.stopPropagation();return}a.untrack(S)&&(a.untrack(_)?pe(!1):I(!1),n.stopPropagation())}},t=n=>{if(!a.untrack(S)||a.untrack(_))return;(n.composedPath?.()??[]).some(d=>d instanceof Element&&d.closest?.(".sr-rail"))||I(!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=()=>oe(`${rt} · ${ke(s)}`),t=()=>{I(i=>!i),V()},n=()=>{pe(i=>!i),a.untrack(S)||(I(!0),V())};window.addEventListener(nt,e),window.addEventListener(Ce,t),window.addEventListener(Ee,n),a.onCleanup(()=>{window.removeEventListener(nt,e),window.removeEventListener(Ce,t),window.removeEventListener(Ee,n)})});let we="",C=0,Fe=0,Je="";const ie=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{}},Ne=()=>{for(const e of c())if(E(e.id))return e},q=(e,t)=>{Ht(`${e}#${Se}${t}`),we=t},ae=()=>{const t=j()?.querySelectorAll("[data-message-id]")??[];if(t.length===0)return"0";const n=t[0].getAttribute("data-message-id")??"",i=t[t.length-1].getAttribute("data-message-id")??"";return`${t.length}:${n}:${i}`},He=(e,t,n,i)=>new Promise(g=>{let d=!1,v,o,u;const f=Date.now();let b=ae(),w=f,y=0;const p=M=>{if(!d){d=!0;try{v?.disconnect()}catch{}clearInterval(o),clearTimeout(u),g(M??null)}},x=M=>{if(t!==C)return p(null);const Z=M?E(e):document.getElementById(`${Se}${e}`);if(Z)return p(Z);if(!M)return;const Xe=ae(),et=Date.now();if(Xe!==b){b=Xe,w=et,y++,i?.(y);return}const vt=y>0?Ot:zt;et-w>vt&&p(null)},L=E(e);if(L)return p(L);v=new MutationObserver(()=>x(!1)),v.observe(document.documentElement,{childList:!0,subtree:!0}),o=setInterval(()=>x(!0),120),u=setTimeout(()=>p(E(e)),Math.max(400,n))}),Ue=()=>{C++,X(!1),z(0),J(0),N(0),te(""),H(!1),oe("已取消跳转")},We=async e=>{if(A()&&ee()===e.id)return;const t=it(s),n=j(),i=E(e.id);if(i&&n){R()&&(le(),t&&q(t,e.id)),ue(i),ie(i);return}const g=Date.now();if(e.id===Je&&g-Fe<Ft){const h=E(e.id);if(h&&n){ue(h),ie(h);return}}if(Fe=g,Je=e.id,Me=e.id,!t){oe(`无法读取当前路由,无法定位 · ${ke(s)}`);return}const d=++C,v=k();X(!0),te(e.id),z(0),J(0),N(0),H(R()),I(!0);const o=Date.now(),u=setInterval(()=>{J(Math.round((Date.now()-o)/1e3))},500);let f=0;try{if(R()){const b=await He(e.id,d,2e4,w=>z(w));if(d!==C||k()!==v)return;if(b){le(),q(t,e.id),ue(b),ie(b);return}if(R()&&le(),H(!1),d!==C||k()!==v)return}const h=Date.now()+Rt;for(let b=0;b<qt&&Date.now()<h;b++){if(d!==C||k()!==v)return;if(N(b+1),we===e.id){const x=Ne()??c()[0];x&&(q(t,x.id),await de(st))}if(d!==C||k()!==v)return;q(t,e.id);let w=0;const y=Math.min(Bt,h-Date.now()),p=await He(e.id,d,y,x=>{w=x,z(f+x)});if(f+=w,d!==C||k()!==v)return;if(p){ue(p),ie(p);return}}d===C&&oe(`未能定位 · ${ke(s)}`)}finally{clearInterval(u),d===C&&(X(!1),z(0),J(0),N(0),te(""),H(!1))}},[R,ve]=a.createSignal(!1);let B=0,G="",xe="";const ht=()=>{const e=j();if(!e)return;const t=e.getBoundingClientRect();for(const n of e.querySelectorAll("[data-message-id]")){const i=n.getBoundingClientRect();if(i.bottom<=t.top+1)continue;const g=n.getAttribute("data-message-id");if(g)return{el:n,id:g,offset:i.top-t.top}}},Ye=e=>{if(!e)return;const t=j();if(!t)return;const n=e.el?.isConnected?e.el:E(e.id);if(!n)return;const i=t.getBoundingClientRect(),g=n.getBoundingClientRect().top-i.top-e.offset;Math.abs(g)>2&&(t.scrollTop+=g)},le=()=>{B++,ve(!1)},V=()=>{const e=k();!e||R()||G!==e&&(c().length<15||mt())},mt=async()=>{if(R())return;const e=it(s),t=c()[0],n=k();if(!e||!t||!n||G===n&&E(t.id))return;const i=++B,g=ht(),d=j(),v=d?d.scrollTop+d.clientHeight>=d.scrollHeight-32:!0;ve(!0);try{q(e,t.id);const o=Date.now()+6e4;let u=ae(),f=Date.now(),h=0;const b=setInterval(()=>Ye(g),150);try{for(;Date.now()<o&&i===B&&!(E(t.id)||(await de(250),i!==B));){const y=Date.now(),p=ae();if(p!==u)u=p,f=y,h=0;else if(y-f>5e3){if(h>=2)break;const x=Ne();if(x&&x.id!==t.id&&(q(e,x.id),await de(st),i!==B))break;q(e,t.id),f=Date.now(),h++}}}finally{clearInterval(b)}if(i!==B)return;const w=!!E(t.id);if(Ye(g),v){const y=j();y&&(y.scrollTop=y.scrollHeight)}w?G=n:xe=n}finally{i===B&&ve(!1)}};let Ge=a.untrack(k);a.createEffect(()=>{const e=k();e!==Ge&&(Ge=e,le(),C++,X(!1),z(0),J(0),N(0),te(""),H(!1))}),a.createEffect(()=>{const e=k(),t=c().length,n=R();if(!e||t<15||n||G===e||xe===e||U||ne!==e)return;const i=setTimeout(()=>V(),1200);a.onCleanup(()=>clearTimeout(i))});const K=a.createMemo(()=>{const e=c(),t=Q().trim().toLowerCase();return t?e.filter(n=>n.text.toLowerCase().includes(t)):e}),[ce,Ve]=a.createSignal(-1),Ke=()=>Ve(-1),Ze=e=>{const t=K();if(t.length===0)return;let n=ce();if(n<0){const i=$();n=t.findIndex(g=>g.id===i),n<0&&(n=e>0?-1:0)}n=Math.min(t.length-1,Math.max(0,n+e)),Ve(n),requestAnimationFrame(()=>{document.querySelector(`.sr-list .sr-turn[data-index="${n}"]`)?.scrollIntoView({block:"nearest"})})},bt=()=>{const e=K()[ce()];e&&We(e)};let ye;const Qe=e=>{e.key==="ArrowDown"?(e.preventDefault(),e.stopPropagation(),Ze(1)):e.key==="ArrowUp"?(e.preventDefault(),e.stopPropagation(),Ze(-1)):e.key==="Enter"&&ce()>=0&&(e.preventDefault(),e.stopPropagation(),bt())};a.createEffect(()=>{A()&&ee()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const wt=[r.createComponent(a.Show,{get when(){return O()},get children(){var e=tt();return r.insert(e,O),e}}),(()=>{var e=Et(),t=e.firstChild,n=t.firstChild,i=n.firstChild,g=i.firstChild,d=i.nextSibling,v=t.nextSibling;return e.addEventListener("mouseleave",()=>{!A()&&!_()&&I(!1)}),e.addEventListener("mouseenter",()=>{I(!0),V()}),r.insert(t,r.createComponent(a.Show,{get when(){return A()},get fallback(){return(()=>{var o=_t();return o.firstChild,r.insert(o,(()=>{var u=r.memo(()=>c().length>0);return()=>u()?` · ${c().length}`:""})(),null),r.insert(o,r.createComponent(a.Show,{get when(){return Q().trim()},get children(){return[" · 匹配 ",r.memo(()=>K().length)]}}),null),o})()},get children(){var o=xt();return r.insert(o,()=>je()?"预热中 · 稍等一下":"加载历史中",null),r.insert(o,(()=>{var u=r.memo(()=>Le()>0);return()=>u()?` · ${Le()} 页`:""})(),null),r.insert(o,(()=>{var u=r.memo(()=>Pe()>0);return()=>u()?` · ${Pe()}s`:""})(),null),o}}),n),i.$$click=()=>pe(!_()),r.insert(g,r.createComponent(a.Show,{get when(){return _()},get fallback(){return[At(),Mt()]},get children(){return[yt(),kt()]}})),d.$$click=()=>{A()?Ue():I(!1)},r.insert(e,r.createComponent(a.Show,{get when(){return c().length>=8},get children(){var o=St(),u=o.firstChild;u.$$keydown=Qe,u.$$input=h=>{Ie(h.currentTarget.value),Ke()};var f=ye;return typeof f=="function"?r.use(f,u):ye=u,r.insert(o,r.createComponent(a.Show,{get when(){return Q()},get children(){var h=$t();return h.$$click=()=>{Ie(""),Ke(),ye?.focus()},h}}),null),r.effect(()=>u.value=Q()),o}}),v),v.$$keydown=Qe,r.insert(v,r.createComponent(a.Show,{get when(){return K().length>0},get fallback(){return(()=>{var o=Tt();return r.insert(o,()=>lt()||`… ${rt}`),o})()},get children(){return r.createComponent(a.For,{get each(){return K()},children:(o,u)=>(()=>{var f=It(),h=f.firstChild,b=h.firstChild;b.firstChild;var w=b.nextSibling,y=h.nextSibling;return f.$$click=()=>{We(o)},r.insert(b,()=>c().indexOf(o)+1,null),r.insert(w,()=>Jt(o.created)),r.insert(h,()=>o.text,null),r.insert(f,r.createComponent(a.Show,{get when(){return r.memo(()=>!!A())()&&ee()===o.id},get children(){var p=Dt();return r.insert(p,()=>je()?"稍等":"加载中"),p}}),y),r.effect(p=>{var x=$()===o.id?"true":"false",L=ce()===u()?"true":"false",M=A()&&ee()===o.id?"true":"false",Z=u();return x!==p.e&&r.setAttribute(f,"data-active",p.e=x),L!==p.t&&r.setAttribute(f,"data-cursor",p.t=L),M!==p.a&&r.setAttribute(f,"data-busy",p.a=M),Z!==p.o&&r.setAttribute(f,"data-index",p.o=Z),p},{e:void 0,t:void 0,a:void 0,o:void 0}),f})()})}})),r.insert(e,r.createComponent(a.Show,{get when(){return r.memo(()=>!!S())()&&O()},get children(){var o=Ct();return r.insert(o,O),o}}),null),r.effect(o=>{var u=S()?"true":"false",f=A()?"true":"false",h=_()?"true":"false",b=S()?"dialog":void 0,w=S()?"会话轮次":void 0,y=_()?"true":"false",p=_()?"取消固定 (⌘⌥P)":"固定面板 (⌘⌥P)",x=_()?"取消固定面板":"固定面板",L=_()?"true":"false",M=A()?"取消跳转 (Esc)":"关闭 (Esc)";return u!==o.e&&r.setAttribute(e,"data-open",o.e=u),f!==o.t&&r.setAttribute(e,"data-jumping",o.t=f),h!==o.a&&r.setAttribute(e,"data-pinned",o.a=h),b!==o.o&&r.setAttribute(e,"role",o.o=b),w!==o.i&&r.setAttribute(e,"aria-label",o.i=w),y!==o.n&&r.setAttribute(i,"data-on",o.n=y),p!==o.s&&r.setAttribute(i,"title",o.s=p),x!==o.h&&r.setAttribute(i,"aria-label",o.h=x),L!==o.r&&r.setAttribute(i,"aria-pressed",o.r=L),M!==o.d&&r.setAttribute(d,"title",o.d=M),o},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0,n:void 0,s:void 0,h:void 0,r:void 0,d:void 0}),e})(),r.createComponent(a.Show,{get when(){return r.memo(()=>!S())()&&O()},get children(){var e=tt();return r.insert(e,O),e}})];return r.createComponent(r.Portal,{get mount(){return document.body},children:wt})}const Yt=F.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","command.pin":"Session turns: pin panel","command.pin.description":"Toggle pinning the turn rail open (⌘⌥P)"}},setup(l){l.add(F.Style,Ut),l.add(F.Slot,{at:"window.bottom",render:()=>r.createComponent(a.Show,{get when(){return l.sessions.current()},get children(){return r.createComponent(Wt,{session:()=>l.sessions.current()})}})}),l.add(F.Command,()=>({id:"jump",title:l.t("command.jump"),description:l.t("command.jump.description"),section:"navigation",bind:"mod+alt+r",slash:{name:"turns"},run:()=>{window.dispatchEvent(new Event(Ce))}})),l.add(F.Command,()=>({id:"pin",title:l.t("command.pin"),description:l.t("command.pin.description"),section:"navigation",bind:"mod+alt+p",slash:{name:"turns-pin"},run:()=>{window.dispatchEvent(new Event(Ee))}}))}});r.delegateEvents(["click","input","keydown"]);exports.default=Yt;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-session-turns",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "OpenCode Desktop 插件:在会话右缘列出每一轮用户提问,点击即可跳转到对应消息。A right-edge rail of user turns; click one to jump to that message.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "vite build",
|
|
41
41
|
"dev": "vite build --watch",
|
|
42
|
-
"prepublishOnly": "npm run build"
|
|
42
|
+
"prepublishOnly": "node scripts/ensure-ci-publish.mjs && npm run build"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@opencode/plugin": "^2.2.6",
|
package/src/gui.tsx
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { Command, Plugin, Slot, Style, usePlugin } from "@opencode/plugin/gui"
|
|
2
|
-
import { For, Show, createEffect, createSignal, onCleanup, onMount, untrack } from "solid-js"
|
|
2
|
+
import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, untrack } from "solid-js"
|
|
3
3
|
import { Portal } from "solid-js/web"
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// constants / helpers
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
|
|
9
|
-
const VERSION = "
|
|
9
|
+
const VERSION = "v21"
|
|
10
10
|
const HASH_PREFIX = "message-"
|
|
11
11
|
const DIAG_EVENT = "session-turns:diag"
|
|
12
12
|
const OPEN_EVENT = "session-turns:open"
|
|
13
|
+
const PIN_EVENT = "session-turns:pin"
|
|
13
14
|
|
|
14
15
|
// paging: a full load walks every page; a light refresh pulls just the newest
|
|
15
16
|
// page (new messages always land at the head of a desc listing).
|
|
@@ -53,7 +54,21 @@ const pad = (value) => String(value).padStart(2, "0")
|
|
|
53
54
|
|
|
54
55
|
const formatTime = (created) => {
|
|
55
56
|
const date = new Date(created ?? 0)
|
|
56
|
-
|
|
57
|
+
const now = new Date()
|
|
58
|
+
const sameDay =
|
|
59
|
+
date.getFullYear() === now.getFullYear() &&
|
|
60
|
+
date.getMonth() === now.getMonth() &&
|
|
61
|
+
date.getDate() === now.getDate()
|
|
62
|
+
if (sameDay) return `${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
63
|
+
const yesterday = new Date(now)
|
|
64
|
+
yesterday.setDate(now.getDate() - 1)
|
|
65
|
+
const wasYesterday =
|
|
66
|
+
date.getFullYear() === yesterday.getFullYear() &&
|
|
67
|
+
date.getMonth() === yesterday.getMonth() &&
|
|
68
|
+
date.getDate() === yesterday.getDate()
|
|
69
|
+
if (wasYesterday) return `昨天 ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
70
|
+
// Older turns need a date: a bare HH:mm is ambiguous in multi-day sessions.
|
|
71
|
+
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
@@ -239,6 +254,73 @@ const CSS = `
|
|
|
239
254
|
color: rgba(255,170,60,.95);
|
|
240
255
|
font-variant-numeric: tabular-nums;
|
|
241
256
|
}
|
|
257
|
+
.sr-head-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
|
|
258
|
+
.sr-iconbtn {
|
|
259
|
+
display: inline-flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
justify-content: center;
|
|
262
|
+
width: 22px;
|
|
263
|
+
height: 22px;
|
|
264
|
+
padding: 0;
|
|
265
|
+
border: 0;
|
|
266
|
+
background: transparent;
|
|
267
|
+
color: inherit;
|
|
268
|
+
cursor: pointer;
|
|
269
|
+
border-radius: 4px;
|
|
270
|
+
opacity: .5;
|
|
271
|
+
}
|
|
272
|
+
.sr-iconbtn svg { width: 13px; height: 13px; }
|
|
273
|
+
.sr-iconbtn:hover { opacity: 1; background: var(--surface-base-hover, rgba(127,127,127,.14)); }
|
|
274
|
+
.sr-iconbtn[data-on="true"] { opacity: .95; color: rgba(255,170,60,.95); }
|
|
275
|
+
.sr-iconbtn:focus-visible { outline: 1px solid currentColor; outline-offset: -1px; }
|
|
276
|
+
/* search box */
|
|
277
|
+
.sr-search {
|
|
278
|
+
position: relative;
|
|
279
|
+
flex-shrink: 0;
|
|
280
|
+
margin: 0 0 6px;
|
|
281
|
+
}
|
|
282
|
+
.sr-search-input {
|
|
283
|
+
width: 100%;
|
|
284
|
+
box-sizing: border-box;
|
|
285
|
+
height: 26px;
|
|
286
|
+
padding: 0 24px 0 8px;
|
|
287
|
+
border: 1px solid rgba(127,127,127,.25);
|
|
288
|
+
border-radius: 6px;
|
|
289
|
+
background: color-mix(in srgb, currentColor 6%, transparent);
|
|
290
|
+
color: inherit;
|
|
291
|
+
font: inherit;
|
|
292
|
+
font-size: 12px;
|
|
293
|
+
outline: none;
|
|
294
|
+
transition: border-color .12s ease, box-shadow .12s ease;
|
|
295
|
+
}
|
|
296
|
+
.sr-search-input::placeholder { color: currentColor; opacity: .4; }
|
|
297
|
+
.sr-search-input:focus {
|
|
298
|
+
border-color: rgba(255,170,60,.6);
|
|
299
|
+
box-shadow: 0 0 0 2px rgba(255,170,60,.14);
|
|
300
|
+
}
|
|
301
|
+
.sr-search-clear {
|
|
302
|
+
position: absolute;
|
|
303
|
+
right: 4px;
|
|
304
|
+
top: 50%;
|
|
305
|
+
transform: translateY(-50%);
|
|
306
|
+
display: inline-flex;
|
|
307
|
+
align-items: center;
|
|
308
|
+
justify-content: center;
|
|
309
|
+
width: 16px;
|
|
310
|
+
height: 16px;
|
|
311
|
+
padding: 0;
|
|
312
|
+
border: 0;
|
|
313
|
+
border-radius: 50%;
|
|
314
|
+
background: transparent;
|
|
315
|
+
color: inherit;
|
|
316
|
+
font-size: 12px;
|
|
317
|
+
line-height: 1;
|
|
318
|
+
cursor: pointer;
|
|
319
|
+
opacity: .5;
|
|
320
|
+
}
|
|
321
|
+
.sr-search-clear:hover { opacity: 1; background: var(--surface-base-hover, rgba(127,127,127,.14)); }
|
|
322
|
+
/* keyboard cursor (↑/↓) */
|
|
323
|
+
.sr-turn[data-cursor="true"] { background: var(--surface-base-hover, rgba(127,127,127,.14)); }
|
|
242
324
|
.sr-close {
|
|
243
325
|
border: 0;
|
|
244
326
|
background: transparent;
|
|
@@ -435,6 +517,12 @@ const CSS = `
|
|
|
435
517
|
white-space: pre-wrap;
|
|
436
518
|
max-width: 260px;
|
|
437
519
|
}
|
|
520
|
+
/* Reduced motion: keep the signals (colors, widths) but drop the animation. */
|
|
521
|
+
@media (prefers-reduced-motion: reduce) {
|
|
522
|
+
.sr-rail, .sr-dash, .sr-text { transition: none !important; }
|
|
523
|
+
.sr-turn[data-busy="true"] .sr-dash { animation: none !important; opacity: 1 !important; }
|
|
524
|
+
.sr-flash-ring { animation: none !important; box-shadow: inset 0 0 0 2px rgba(255,170,60,.5) !important; }
|
|
525
|
+
}
|
|
438
526
|
`
|
|
439
527
|
|
|
440
528
|
// ---------------------------------------------------------------------------
|
|
@@ -448,6 +536,10 @@ function Rail(props) {
|
|
|
448
536
|
const [open, setOpen] = createSignal(false)
|
|
449
537
|
const [note, setNote] = createSignal("")
|
|
450
538
|
const [status, setStatus] = createSignal("")
|
|
539
|
+
// Search box over the turn list; empty means show everything.
|
|
540
|
+
const [query, setQuery] = createSignal("")
|
|
541
|
+
// Pinned panels stay open when the pointer leaves (toggle: ⌘⌥P / /turns pin).
|
|
542
|
+
const [pinned, setPinned] = createSignal(false)
|
|
451
543
|
// Jump progress. While `jumping` is true the panel stays pinned open and the
|
|
452
544
|
// header reports what the host is doing; `busyID` marks the target row.
|
|
453
545
|
const [jumping, setJumping] = createSignal(false)
|
|
@@ -662,9 +754,11 @@ function Rail(props) {
|
|
|
662
754
|
|
|
663
755
|
// ---- active-turn tracking ----------------------------------------------
|
|
664
756
|
// Driven by scroll events (captured at document level so it also sees the
|
|
665
|
-
// real scroller without querying first) plus one rAF-throttled pass
|
|
666
|
-
//
|
|
667
|
-
//
|
|
757
|
+
// real scroller without querying first) plus one rAF-throttled pass. An
|
|
758
|
+
// IntersectionObserver on the timeline rows replaces the old 1.5s interval:
|
|
759
|
+
// a row leaving the sentinel line at the top of the viewport means the turn
|
|
760
|
+
// under it became active, with zero work when nothing moves (and none at
|
|
761
|
+
// all in a background window, where rAF is throttled to zero).
|
|
668
762
|
let scrollerCache
|
|
669
763
|
const scroller = () => {
|
|
670
764
|
if (scrollerCache?.isConnected) return scrollerCache
|
|
@@ -672,6 +766,35 @@ function Rail(props) {
|
|
|
672
766
|
return scrollerCache
|
|
673
767
|
}
|
|
674
768
|
|
|
769
|
+
// Rows already wired to the observer, keyed by element so history paging
|
|
770
|
+
// (rows mounting/unmounting) never re-observes the same node.
|
|
771
|
+
const observed = new WeakSet()
|
|
772
|
+
let rowObserver
|
|
773
|
+
const ensureObserver = () => {
|
|
774
|
+
if (rowObserver) return rowObserver
|
|
775
|
+
rowObserver = new IntersectionObserver(
|
|
776
|
+
(entries) => {
|
|
777
|
+
// Any intersection change along the sentinel line can shift which turn
|
|
778
|
+
// is topmost; recompute from the DOM (rAF-throttled, cheap).
|
|
779
|
+
if (entries.length === 0) return
|
|
780
|
+
scheduleTrack()
|
|
781
|
+
},
|
|
782
|
+
{ rootMargin: "-40px 0px 0px 0px", threshold: 0 },
|
|
783
|
+
)
|
|
784
|
+
return rowObserver
|
|
785
|
+
}
|
|
786
|
+
// Wire up rows that appeared since the last pass. Called from trackActive,
|
|
787
|
+
// so it only runs while the document is actually scrolling (rAF-gated).
|
|
788
|
+
const observeRows = () => {
|
|
789
|
+
const observer = ensureObserver()
|
|
790
|
+
for (const row of document.querySelectorAll("[data-message-id]")) {
|
|
791
|
+
if (!observed.has(row)) {
|
|
792
|
+
observed.add(row)
|
|
793
|
+
observer.observe(row)
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
675
798
|
const trackActive = () => {
|
|
676
799
|
const sc = scroller()
|
|
677
800
|
if (!sc) return
|
|
@@ -681,7 +804,7 @@ function Rail(props) {
|
|
|
681
804
|
for (const row of rows) {
|
|
682
805
|
if (row.getBoundingClientRect().bottom >= top) {
|
|
683
806
|
const id = row.getAttribute("data-message-id")
|
|
684
|
-
if (id) setActive(id)
|
|
807
|
+
if (id && id !== active()) setActive(id)
|
|
685
808
|
break
|
|
686
809
|
}
|
|
687
810
|
}
|
|
@@ -694,16 +817,25 @@ function Rail(props) {
|
|
|
694
817
|
requestAnimationFrame(() => {
|
|
695
818
|
rafPending = false
|
|
696
819
|
trackActive()
|
|
820
|
+
observeRows()
|
|
697
821
|
})
|
|
698
822
|
}
|
|
699
823
|
|
|
700
824
|
onMount(() => {
|
|
701
825
|
const onScroll = () => scheduleTrack()
|
|
702
826
|
document.addEventListener("scroll", onScroll, true)
|
|
703
|
-
|
|
827
|
+
// One slow liveness pass only while the tab is visible: catches rows that
|
|
828
|
+
// mount without a scroll event and reconnects after DOM swaps, without
|
|
829
|
+
// burning cycles in background windows (rAF never fires there, and the
|
|
830
|
+
// interval self-pauses via document.hidden).
|
|
831
|
+
const fallback = setInterval(() => {
|
|
832
|
+
if (document.hidden) return
|
|
833
|
+
scheduleTrack()
|
|
834
|
+
}, 4000)
|
|
704
835
|
onCleanup(() => {
|
|
705
836
|
document.removeEventListener("scroll", onScroll, true)
|
|
706
837
|
clearInterval(fallback)
|
|
838
|
+
rowObserver?.disconnect()
|
|
707
839
|
})
|
|
708
840
|
})
|
|
709
841
|
|
|
@@ -731,7 +863,12 @@ function Rail(props) {
|
|
|
731
863
|
return
|
|
732
864
|
}
|
|
733
865
|
if (untrack(open)) {
|
|
734
|
-
|
|
866
|
+
// Unpin first when pinned; a second Esc then closes the panel.
|
|
867
|
+
if (untrack(pinned)) {
|
|
868
|
+
setPinned(false)
|
|
869
|
+
} else {
|
|
870
|
+
setOpen(false)
|
|
871
|
+
}
|
|
735
872
|
// Only swallow Esc while the rail is actually open, so the host keeps
|
|
736
873
|
// its own Escape handling the rest of the time.
|
|
737
874
|
event.stopPropagation()
|
|
@@ -739,6 +876,7 @@ function Rail(props) {
|
|
|
739
876
|
}
|
|
740
877
|
const onPointerDown = (event) => {
|
|
741
878
|
if (!untrack(open)) return
|
|
879
|
+
if (untrack(pinned)) return
|
|
742
880
|
const path = event.composedPath?.() ?? []
|
|
743
881
|
const inside = path.some((node) => node instanceof Element && node.closest?.(".sr-rail"))
|
|
744
882
|
if (!inside) setOpen(false)
|
|
@@ -753,17 +891,27 @@ function Rail(props) {
|
|
|
753
891
|
|
|
754
892
|
onMount(() => {
|
|
755
893
|
const onDiag = () => flash(`${VERSION} · ${diagnostics(ctx)}`)
|
|
756
|
-
// /turns toggles the panel;
|
|
757
|
-
//
|
|
894
|
+
// /turns toggles the panel; ⌘⌥P / the pin button toggles "pinned" (stay
|
|
895
|
+
// open when the pointer leaves). Esc / outside click / × still close an
|
|
896
|
+
// unpinned panel.
|
|
758
897
|
const onOpen = () => {
|
|
759
898
|
setOpen((value) => !value)
|
|
760
899
|
maybePrewarm()
|
|
761
900
|
}
|
|
901
|
+
const onPin = () => {
|
|
902
|
+
setPinned((value) => !value)
|
|
903
|
+
if (!untrack(open)) {
|
|
904
|
+
setOpen(true)
|
|
905
|
+
maybePrewarm()
|
|
906
|
+
}
|
|
907
|
+
}
|
|
762
908
|
window.addEventListener(DIAG_EVENT, onDiag)
|
|
763
909
|
window.addEventListener(OPEN_EVENT, onOpen)
|
|
910
|
+
window.addEventListener(PIN_EVENT, onPin)
|
|
764
911
|
onCleanup(() => {
|
|
765
912
|
window.removeEventListener(DIAG_EVENT, onDiag)
|
|
766
913
|
window.removeEventListener(OPEN_EVENT, onOpen)
|
|
914
|
+
window.removeEventListener(PIN_EVENT, onPin)
|
|
767
915
|
})
|
|
768
916
|
// No startup toast: the plugin should be invisible when everything works.
|
|
769
917
|
})
|
|
@@ -1201,8 +1349,64 @@ function Rail(props) {
|
|
|
1201
1349
|
onCleanup(() => clearTimeout(timer))
|
|
1202
1350
|
})
|
|
1203
1351
|
|
|
1204
|
-
//
|
|
1205
|
-
//
|
|
1352
|
+
// ---- search / keyboard navigation --------------------------------------
|
|
1353
|
+
// Filtered view of the turns; identity-stable so <For> keeps DOM nodes.
|
|
1354
|
+
const visibleTurns = createMemo(() => {
|
|
1355
|
+
const list = turns()
|
|
1356
|
+
const needle = query().trim().toLowerCase()
|
|
1357
|
+
if (!needle) return list
|
|
1358
|
+
return list.filter((turn) => turn.text.toLowerCase().includes(needle))
|
|
1359
|
+
})
|
|
1360
|
+
|
|
1361
|
+
// The row the ↑/↓ keys currently point at (index within visibleTurns).
|
|
1362
|
+
const [cursorIndex, setCursorIndex] = createSignal(-1)
|
|
1363
|
+
const clearCursor = () => setCursorIndex(-1)
|
|
1364
|
+
|
|
1365
|
+
const moveCursor = (delta) => {
|
|
1366
|
+
const list = visibleTurns()
|
|
1367
|
+
if (list.length === 0) return
|
|
1368
|
+
let index = cursorIndex()
|
|
1369
|
+
if (index < 0) {
|
|
1370
|
+
// Start from the active turn when the cursor is fresh, so ↓ walks to the
|
|
1371
|
+
// next turn relative to where the viewport already is.
|
|
1372
|
+
const activeID = active()
|
|
1373
|
+
index = list.findIndex((turn) => turn.id === activeID)
|
|
1374
|
+
if (index < 0) index = delta > 0 ? -1 : 0
|
|
1375
|
+
}
|
|
1376
|
+
index = Math.min(list.length - 1, Math.max(0, index + delta))
|
|
1377
|
+
setCursorIndex(index)
|
|
1378
|
+
requestAnimationFrame(() => {
|
|
1379
|
+
document
|
|
1380
|
+
.querySelector(`.sr-list .sr-turn[data-index="${index}"]`)
|
|
1381
|
+
?.scrollIntoView({ block: "nearest" })
|
|
1382
|
+
})
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const activateCursor = () => {
|
|
1386
|
+
const turn = visibleTurns()[cursorIndex()]
|
|
1387
|
+
if (!turn) return
|
|
1388
|
+
void jump(turn)
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// The search input, for focus round-trips.
|
|
1392
|
+
let searchInput: HTMLInputElement | undefined
|
|
1393
|
+
const listKeyDown = (event) => {
|
|
1394
|
+
if (event.key === "ArrowDown") {
|
|
1395
|
+
event.preventDefault()
|
|
1396
|
+
event.stopPropagation()
|
|
1397
|
+
moveCursor(1)
|
|
1398
|
+
} else if (event.key === "ArrowUp") {
|
|
1399
|
+
event.preventDefault()
|
|
1400
|
+
event.stopPropagation()
|
|
1401
|
+
moveCursor(-1)
|
|
1402
|
+
} else if (event.key === "Enter" && cursorIndex() >= 0) {
|
|
1403
|
+
event.preventDefault()
|
|
1404
|
+
event.stopPropagation()
|
|
1405
|
+
activateCursor()
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Keep the highlighted row visible while a jump pages through history.
|
|
1206
1410
|
createEffect(() => {
|
|
1207
1411
|
if (!jumping()) return
|
|
1208
1412
|
if (!busyID()) return
|
|
@@ -1224,22 +1428,30 @@ function Rail(props) {
|
|
|
1224
1428
|
class="sr-rail"
|
|
1225
1429
|
data-open={open() ? "true" : "false"}
|
|
1226
1430
|
data-jumping={jumping() ? "true" : "false"}
|
|
1431
|
+
data-pinned={pinned() ? "true" : "false"}
|
|
1432
|
+
role={open() ? "dialog" : undefined}
|
|
1433
|
+
aria-label={open() ? "会话轮次" : undefined}
|
|
1227
1434
|
onMouseEnter={() => {
|
|
1228
1435
|
setOpen(true)
|
|
1229
1436
|
maybePrewarm()
|
|
1230
1437
|
}}
|
|
1231
|
-
// Keep the panel pinned while a long history jump runs
|
|
1232
|
-
// mouse drifting off the rail hides
|
|
1233
|
-
// waiting for. Esc / × still close
|
|
1438
|
+
// Keep the panel pinned while a long history jump runs or when the
|
|
1439
|
+
// user pinned it; otherwise the mouse drifting off the rail hides
|
|
1440
|
+
// exactly the progress the user is waiting for. Esc / × still close
|
|
1441
|
+
// (or cancel) it.
|
|
1234
1442
|
onMouseLeave={() => {
|
|
1235
|
-
if (!jumping()) setOpen(false)
|
|
1443
|
+
if (!jumping() && !pinned()) setOpen(false)
|
|
1236
1444
|
}}
|
|
1237
1445
|
>
|
|
1238
1446
|
<div class="sr-head">
|
|
1239
1447
|
<Show
|
|
1240
1448
|
when={jumping()}
|
|
1241
1449
|
fallback={
|
|
1242
|
-
<span class="sr-title"
|
|
1450
|
+
<span class="sr-title">
|
|
1451
|
+
会话轮次
|
|
1452
|
+
{turns().length > 0 ? ` · ${turns().length}` : ""}
|
|
1453
|
+
<Show when={query().trim()}> · 匹配 {visibleTurns().length}</Show>
|
|
1454
|
+
</span>
|
|
1243
1455
|
}
|
|
1244
1456
|
>
|
|
1245
1457
|
<span class="sr-title sr-progress">
|
|
@@ -1248,31 +1460,93 @@ function Rail(props) {
|
|
|
1248
1460
|
{jumpElapsed() > 0 ? ` · ${jumpElapsed()}s` : ""}
|
|
1249
1461
|
</span>
|
|
1250
1462
|
</Show>
|
|
1251
|
-
<
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1463
|
+
<span class="sr-head-actions">
|
|
1464
|
+
<button
|
|
1465
|
+
type="button"
|
|
1466
|
+
class="sr-iconbtn"
|
|
1467
|
+
data-on={pinned() ? "true" : "false"}
|
|
1468
|
+
title={pinned() ? "取消固定 (⌘⌥P)" : "固定面板 (⌘⌥P)"}
|
|
1469
|
+
aria-label={pinned() ? "取消固定面板" : "固定面板"}
|
|
1470
|
+
aria-pressed={pinned() ? "true" : "false"}
|
|
1471
|
+
onClick={() => setPinned(!pinned())}
|
|
1472
|
+
>
|
|
1473
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1474
|
+
<Show
|
|
1475
|
+
when={pinned()}
|
|
1476
|
+
fallback={
|
|
1477
|
+
<>
|
|
1478
|
+
<path d="M9.5 2.5l4 4-2.6.9-1.4 3.5-4.4-4.4L8.6 5.1z" />
|
|
1479
|
+
<path d="M5.1 6.5L2.5 9.1l4.4 4.4 2.6-2.6" />
|
|
1480
|
+
</>
|
|
1481
|
+
}
|
|
1482
|
+
>
|
|
1483
|
+
<path d="M6 3l7 7-3.5.5L7 13 3 9l2.5-2.5z" />
|
|
1484
|
+
<path d="M4 12l-1.5 1.5" />
|
|
1485
|
+
</Show>
|
|
1486
|
+
</svg>
|
|
1487
|
+
</button>
|
|
1488
|
+
<button
|
|
1489
|
+
type="button"
|
|
1490
|
+
class="sr-close"
|
|
1491
|
+
title={jumping() ? "取消跳转 (Esc)" : "关闭 (Esc)"}
|
|
1492
|
+
onClick={() => {
|
|
1493
|
+
if (jumping()) cancelJump()
|
|
1494
|
+
else setOpen(false)
|
|
1495
|
+
}}
|
|
1496
|
+
>
|
|
1497
|
+
×
|
|
1498
|
+
</button>
|
|
1499
|
+
</span>
|
|
1262
1500
|
</div>
|
|
1263
|
-
<
|
|
1264
|
-
<
|
|
1265
|
-
<
|
|
1501
|
+
<Show when={turns().length >= 8}>
|
|
1502
|
+
<div class="sr-search">
|
|
1503
|
+
<input
|
|
1504
|
+
ref={searchInput}
|
|
1505
|
+
type="text"
|
|
1506
|
+
class="sr-search-input"
|
|
1507
|
+
placeholder="搜索轮次…"
|
|
1508
|
+
value={query()}
|
|
1509
|
+
onInput={(event) => {
|
|
1510
|
+
setQuery(event.currentTarget.value)
|
|
1511
|
+
clearCursor()
|
|
1512
|
+
}}
|
|
1513
|
+
onKeyDown={listKeyDown}
|
|
1514
|
+
/>
|
|
1515
|
+
<Show when={query()}>
|
|
1516
|
+
<button
|
|
1517
|
+
type="button"
|
|
1518
|
+
class="sr-search-clear"
|
|
1519
|
+
aria-label="清除搜索"
|
|
1520
|
+
onClick={() => {
|
|
1521
|
+
setQuery("")
|
|
1522
|
+
clearCursor()
|
|
1523
|
+
searchInput?.focus()
|
|
1524
|
+
}}
|
|
1525
|
+
>
|
|
1526
|
+
×
|
|
1527
|
+
</button>
|
|
1528
|
+
</Show>
|
|
1529
|
+
</div>
|
|
1530
|
+
</Show>
|
|
1531
|
+
<div
|
|
1532
|
+
class="sr-list"
|
|
1533
|
+
onKeyDown={listKeyDown}
|
|
1534
|
+
tabIndex={-1}
|
|
1535
|
+
>
|
|
1536
|
+
<Show when={visibleTurns().length > 0} fallback={<div class="sr-note">{note() || `… ${VERSION}`}</div>}>
|
|
1537
|
+
<For each={visibleTurns()}>
|
|
1266
1538
|
{(turn, index) => (
|
|
1267
1539
|
<button
|
|
1268
1540
|
type="button"
|
|
1269
1541
|
class="sr-turn"
|
|
1270
1542
|
data-active={active() === turn.id ? "true" : "false"}
|
|
1543
|
+
data-cursor={cursorIndex() === index() ? "true" : "false"}
|
|
1271
1544
|
data-busy={jumping() && busyID() === turn.id ? "true" : "false"}
|
|
1545
|
+
data-index={index()}
|
|
1272
1546
|
onClick={() => void jump(turn)}
|
|
1273
1547
|
>
|
|
1274
1548
|
<span class="sr-text">
|
|
1275
|
-
<span class="sr-num">#{
|
|
1549
|
+
<span class="sr-num">#{turns().indexOf(turn) + 1}</span>
|
|
1276
1550
|
<span class="sr-time">{formatTime(turn.created)}</span>
|
|
1277
1551
|
{turn.text}
|
|
1278
1552
|
</span>
|
|
@@ -1312,6 +1586,8 @@ export default Plugin.define({
|
|
|
1312
1586
|
en: {
|
|
1313
1587
|
"command.jump": "Session turns: show / jump",
|
|
1314
1588
|
"command.jump.description": "Reveal the turn rail; click a turn to jump to that message",
|
|
1589
|
+
"command.pin": "Session turns: pin panel",
|
|
1590
|
+
"command.pin.description": "Toggle pinning the turn rail open (⌘⌥P)",
|
|
1315
1591
|
},
|
|
1316
1592
|
},
|
|
1317
1593
|
setup(ctx) {
|
|
@@ -1333,7 +1609,6 @@ export default Plugin.define({
|
|
|
1333
1609
|
id: "jump",
|
|
1334
1610
|
title: ctx.t("command.jump"),
|
|
1335
1611
|
description: ctx.t("command.jump.description"),
|
|
1336
|
-
group: ctx.t("command.jump"),
|
|
1337
1612
|
section: "navigation",
|
|
1338
1613
|
bind: "mod+alt+r",
|
|
1339
1614
|
slash: { name: "turns" },
|
|
@@ -1341,5 +1616,17 @@ export default Plugin.define({
|
|
|
1341
1616
|
window.dispatchEvent(new Event(OPEN_EVENT))
|
|
1342
1617
|
},
|
|
1343
1618
|
}))
|
|
1619
|
+
|
|
1620
|
+
ctx.add(Command, () => ({
|
|
1621
|
+
id: "pin",
|
|
1622
|
+
title: ctx.t("command.pin"),
|
|
1623
|
+
description: ctx.t("command.pin.description"),
|
|
1624
|
+
section: "navigation",
|
|
1625
|
+
bind: "mod+alt+p",
|
|
1626
|
+
slash: { name: "turns-pin" },
|
|
1627
|
+
run: () => {
|
|
1628
|
+
window.dispatchEvent(new Event(PIN_EVENT))
|
|
1629
|
+
},
|
|
1630
|
+
}))
|
|
1344
1631
|
},
|
|
1345
1632
|
})
|