opencode-session-turns 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/gui-experimental.cjs +113 -42
- package/package.json +1 -1
- package/src/gui.tsx +283 -104
package/README.md
CHANGED
|
@@ -20,8 +20,11 @@ click a turn to jump straight to that message.
|
|
|
20
20
|
## 功能
|
|
21
21
|
|
|
22
22
|
- 列出当前会话的每一轮用户提问(从会话消息接口分页拉取,`type=user`)。
|
|
23
|
-
-
|
|
23
|
+
- 鼠标移到窗口右缘的细条上自动展开,移开自动收起;长历史跳转期间面板保持展开。
|
|
24
24
|
- 点击任意一轮,**跳转到对应消息**——包括很久以前、尚未渲染的历史轮次。
|
|
25
|
+
- 深历史需要逐页加载时,头部实时显示「跳转中 · 第 N 页」,目标轮次琥珀色脉冲,
|
|
26
|
+
底部状态条同步提示;按 `Esc` 或点 × 可随时取消。
|
|
27
|
+
- 定位成功后精准描边闪烁**那一条用户气泡**(跟随气泡自身圆角 + 柔光),不再整行填色。
|
|
25
28
|
- 自动高亮当前视口所在的轮次。
|
|
26
29
|
- 斜杠命令 `/turns`、快捷键 `⌘⌥R`(Windows/Linux 为 `Ctrl+Alt+R`)展开轮次条。
|
|
27
30
|
|
|
@@ -62,6 +65,8 @@ OpenCode Desktop 2.2.6 有几个必须绕开的陷阱:
|
|
|
62
65
|
`window.location`。
|
|
63
66
|
3. 宿主每次 seek 只向前加载**约一页(~5 轮)**历史,所以插件会交替把 hash 指向
|
|
64
67
|
「已加载的最早一轮」和目标,逐页把历史翻出来,直到目标行挂载。
|
|
68
|
+
每一轮监听行集合指纹,宿主一旦翻入新一页就立即进入下一轮(不再固定等待),
|
|
69
|
+
并在面板头部显示页码进度;总预算 45 秒,`Esc` 可取消。
|
|
65
70
|
|
|
66
71
|
已挂载的行则直接用本地滚动,瞬时定位。
|
|
67
72
|
|
|
@@ -1,53 +1,51 @@
|
|
|
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 a=require("solid-js/web"),A=require("@opencode/plugin/gui"),i=require("solid-js");var Lt=a.template("<div class=sr-status>"),It=a.template('<span class="sr-title sr-progress">跳转中'),Mt=a.template("<div class=sr-rail><div class=sr-head><button type=button class=sr-close>×</button></div><div class=sr-list>"),Pt=a.template("<span class=sr-title>会话轮次"),Dt=a.template("<div class=sr-note>"),jt=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 G="v12",wt="message-",ht="session-turns:diag",Y="session-turns:open",qt=40,Rt=200,Ot=50,Jt=120,V=45e3,Nt=4e3,zt=300,Ft=800;let xt=0,Z="",X="";const gt=o=>{const n=String(o?.text??"").split(`
|
|
2
|
+
`).map(g=>g.trim()).find(g=>g.length>0);if(n)return n;const f=o?.files?.length??0;return f>0?`[附件 ×${f}]`:"(空消息)"},mt=o=>String(o).padStart(2,"0"),Ht=o=>{const n=new Date(o??0);return`${mt(n.getHours())}:${mt(n.getMinutes())}`},bt=o=>new Promise(n=>setTimeout(n,o)),vt=()=>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 f=getComputedStyle(n);if(/(auto|scroll)/.test(f.overflowY)&&n.scrollHeight>n.clientHeight+1)return n;n=n.parentElement}})(),$=o=>document.getElementById(`${wt}${o}`)??[...document.querySelectorAll("[data-message-id]")].find(n=>n.getAttribute("data-message-id")===o),W=o=>{const n=vt(),f=[...document.querySelectorAll("[data-message-id]")],g=f[0]?.getAttribute("data-message-id")??"",j=X?` · 目标 ${$(X)?"在":"不在"} DOM`:"",C=g?` · 行ID ${g.slice(0,20)}`:"";let E="(空)";try{E=o.router.path()||"(空)"}catch{}return`轮次 ${xt} · 容器 ${n?"OK":"缺失"} · DOM ${f.length} 行${j}${C} · 路由 ${E} · hash ${window.location.hash||"(空)"}${Z?` · ${Z}`:""}`},K=(o,n)=>{n.scrollIntoView({block:"start"});const f=o.querySelector("[data-session-title]"),g=f instanceof HTMLElement?f.offsetHeight:0;g&&(o.scrollTop-=g+12)},Ut=o=>{let n="";try{n=o.router.path()||""}catch{}return n.startsWith("/")?n:""},Bt=o=>{const n=document.createElement("a");n.setAttribute("href",o),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()}},Gt=`
|
|
3
3
|
.sr-rail {
|
|
4
4
|
position: fixed;
|
|
5
5
|
top: 50%;
|
|
6
6
|
transform: translateY(-50%);
|
|
7
|
-
right:
|
|
8
|
-
max-height:
|
|
9
|
-
width:
|
|
7
|
+
right: 14px;
|
|
8
|
+
max-height: 66vh;
|
|
9
|
+
width: 16px;
|
|
10
10
|
display: flex;
|
|
11
11
|
flex-direction: column;
|
|
12
|
-
align-items: flex-end;
|
|
13
12
|
justify-content: center;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
overflow-y: auto;
|
|
17
|
-
overflow-x: hidden;
|
|
18
|
-
scrollbar-width: none;
|
|
19
|
-
/* keep wheel scrolling from chaining into the host timeline when the rail
|
|
20
|
-
reaches its own end */
|
|
21
|
-
overscroll-behavior: contain;
|
|
13
|
+
padding: 6px 2px;
|
|
14
|
+
overflow: hidden;
|
|
22
15
|
z-index: 400;
|
|
23
16
|
border-radius: 10px;
|
|
17
|
+
border: 1px solid transparent;
|
|
24
18
|
color: var(--text-base, currentColor);
|
|
25
19
|
background: transparent;
|
|
26
|
-
transition: width .18s ease, background .18s ease, box-shadow .18s ease;
|
|
20
|
+
transition: width .18s ease, background .18s ease, box-shadow .18s ease, border-color .18s ease;
|
|
27
21
|
}
|
|
28
|
-
.sr-rail::-webkit-scrollbar { display: none; }
|
|
29
22
|
.sr-rail[data-open="true"] {
|
|
30
|
-
width:
|
|
31
|
-
padding: 10px
|
|
23
|
+
width: 340px;
|
|
24
|
+
padding: 10px;
|
|
32
25
|
justify-content: flex-start;
|
|
33
|
-
background: var(--surface-base, rgba(
|
|
34
|
-
backdrop-filter: blur(
|
|
35
|
-
-webkit-backdrop-filter: blur(
|
|
36
|
-
|
|
26
|
+
background: var(--surface-base, rgba(24,24,26,.86));
|
|
27
|
+
backdrop-filter: blur(16px);
|
|
28
|
+
-webkit-backdrop-filter: blur(16px);
|
|
29
|
+
border-color: rgba(127,127,127,.25);
|
|
30
|
+
box-shadow: 0 8px 32px rgba(0,0,0,.28);
|
|
37
31
|
}
|
|
38
32
|
.sr-head {
|
|
39
33
|
display: none;
|
|
40
34
|
align-items: center;
|
|
41
35
|
justify-content: space-between;
|
|
42
36
|
gap: 8px;
|
|
43
|
-
padding: 0 2px
|
|
44
|
-
margin-bottom:
|
|
45
|
-
border-bottom: 1px solid rgba(127,127,127,.
|
|
37
|
+
padding: 0 2px 8px;
|
|
38
|
+
margin-bottom: 6px;
|
|
39
|
+
border-bottom: 1px solid rgba(127,127,127,.2);
|
|
46
40
|
flex-shrink: 0;
|
|
47
|
-
width: 100%;
|
|
48
41
|
}
|
|
49
42
|
.sr-rail[data-open="true"] .sr-head { display: flex; }
|
|
50
|
-
.sr-title { font-size:
|
|
43
|
+
.sr-title { font-size: 12px; opacity: .6; white-space: nowrap; }
|
|
44
|
+
.sr-title.sr-progress {
|
|
45
|
+
opacity: .95;
|
|
46
|
+
color: rgba(255,170,60,.95);
|
|
47
|
+
font-variant-numeric: tabular-nums;
|
|
48
|
+
}
|
|
51
49
|
.sr-close {
|
|
52
50
|
border: 0;
|
|
53
51
|
background: transparent;
|
|
@@ -62,6 +60,26 @@
|
|
|
62
60
|
flex-shrink: 0;
|
|
63
61
|
}
|
|
64
62
|
.sr-close:hover { opacity: 1; background: var(--surface-base-hover, rgba(127,127,127,.14)); }
|
|
63
|
+
/* the scrollable list; the header stays fixed above it */
|
|
64
|
+
.sr-list {
|
|
65
|
+
flex: 1;
|
|
66
|
+
min-height: 0;
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
gap: 2px;
|
|
71
|
+
overflow-y: auto;
|
|
72
|
+
overflow-x: hidden;
|
|
73
|
+
scrollbar-width: none;
|
|
74
|
+
}
|
|
75
|
+
.sr-rail[data-open="true"] .sr-list { justify-content: flex-start; gap: 1px; scrollbar-width: thin; }
|
|
76
|
+
.sr-list::-webkit-scrollbar { display: none; }
|
|
77
|
+
.sr-rail[data-open="true"] .sr-list::-webkit-scrollbar { display: block; width: 8px; }
|
|
78
|
+
.sr-list::-webkit-scrollbar-track { background: transparent; }
|
|
79
|
+
.sr-rail[data-open="true"] .sr-list::-webkit-scrollbar-thumb {
|
|
80
|
+
background: rgba(127,127,127,.35);
|
|
81
|
+
border-radius: 4px;
|
|
82
|
+
}
|
|
65
83
|
.sr-turn {
|
|
66
84
|
display: flex;
|
|
67
85
|
align-items: center;
|
|
@@ -77,6 +95,15 @@
|
|
|
77
95
|
font: inherit;
|
|
78
96
|
border-radius: 4px;
|
|
79
97
|
}
|
|
98
|
+
/* expanded: dash flips to a left-side indicator, content reads left-to-right */
|
|
99
|
+
.sr-rail[data-open="true"] .sr-turn {
|
|
100
|
+
flex-direction: row-reverse;
|
|
101
|
+
justify-content: flex-start;
|
|
102
|
+
gap: 9px;
|
|
103
|
+
padding: 5px 8px;
|
|
104
|
+
min-height: 0;
|
|
105
|
+
border-radius: 6px;
|
|
106
|
+
}
|
|
80
107
|
.sr-turn:focus-visible, .sr-close:focus-visible { outline: 1px solid currentColor; outline-offset: -1px; }
|
|
81
108
|
.sr-dash {
|
|
82
109
|
display: block;
|
|
@@ -86,10 +113,13 @@
|
|
|
86
113
|
background: currentColor;
|
|
87
114
|
opacity: .28;
|
|
88
115
|
flex-shrink: 0;
|
|
89
|
-
transition: opacity .12s ease, width .12s ease;
|
|
116
|
+
transition: opacity .12s ease, width .12s ease, height .12s ease;
|
|
90
117
|
}
|
|
91
118
|
.sr-turn:hover .sr-dash,
|
|
92
119
|
.sr-turn[data-active="true"] .sr-dash { opacity: .95; width: 18px; }
|
|
120
|
+
.sr-rail[data-open="true"] .sr-dash { width: 3px; height: 16px; }
|
|
121
|
+
.sr-rail[data-open="true"] .sr-turn:hover .sr-dash,
|
|
122
|
+
.sr-rail[data-open="true"] .sr-turn[data-active="true"] .sr-dash { width: 3px; height: 22px; }
|
|
93
123
|
.sr-text {
|
|
94
124
|
display: none;
|
|
95
125
|
margin-right: 10px;
|
|
@@ -105,22 +135,63 @@
|
|
|
105
135
|
opacity: .45;
|
|
106
136
|
background: transparent;
|
|
107
137
|
}
|
|
108
|
-
.sr-rail[data-open="true"] .sr-text {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
.sr-rail[data-open="true"] .sr-text {
|
|
139
|
+
display: -webkit-box;
|
|
140
|
+
-webkit-line-clamp: 2;
|
|
141
|
+
-webkit-box-orient: vertical;
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
flex: 1;
|
|
144
|
+
min-width: 0;
|
|
145
|
+
margin: 0;
|
|
146
|
+
padding: 0;
|
|
147
|
+
text-align: left;
|
|
148
|
+
white-space: normal;
|
|
149
|
+
line-height: 1.45;
|
|
150
|
+
max-width: none;
|
|
151
|
+
opacity: .8;
|
|
152
|
+
}
|
|
153
|
+
.sr-num, .sr-time {
|
|
154
|
+
opacity: .55;
|
|
155
|
+
margin-right: 6px;
|
|
156
|
+
font-variant-numeric: tabular-nums;
|
|
113
157
|
}
|
|
114
|
-
.sr-turn
|
|
158
|
+
.sr-turn:hover { background: var(--surface-base-hover, rgba(127,127,127,.14)); }
|
|
159
|
+
.sr-rail[data-open="true"] .sr-turn:hover .sr-text,
|
|
160
|
+
.sr-rail[data-open="true"] .sr-turn[data-active="true"] .sr-text { opacity: 1; }
|
|
161
|
+
.sr-turn[data-active="true"] { font-weight: 500; }
|
|
162
|
+
/* the turn currently being jumped to */
|
|
163
|
+
.sr-turn[data-busy="true"] { background: rgba(255,170,60,.12); }
|
|
164
|
+
.sr-turn[data-busy="true"] .sr-dash {
|
|
115
165
|
opacity: 1;
|
|
116
|
-
|
|
117
|
-
|
|
166
|
+
width: 3px;
|
|
167
|
+
height: 22px;
|
|
168
|
+
background: rgba(255,170,60,.95);
|
|
169
|
+
animation: srBusy 1s ease-in-out infinite;
|
|
170
|
+
}
|
|
171
|
+
.sr-turn[data-busy="false"] .sr-dash { animation: none; }
|
|
172
|
+
@keyframes srBusy {
|
|
173
|
+
0%, 100% { opacity: .35; }
|
|
174
|
+
50% { opacity: 1; }
|
|
118
175
|
}
|
|
119
|
-
/*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
176
|
+
/* pulse a landed bubble: outline follows the bubble's own border-radius, so no
|
|
177
|
+
shape is imposed and the message background stays untouched */
|
|
178
|
+
.sr-flash-ring { animation: srRing 1.8s ease-out 1; }
|
|
179
|
+
@keyframes srRing {
|
|
180
|
+
0% {
|
|
181
|
+
outline: 2px solid rgba(255,170,60,.95);
|
|
182
|
+
outline-offset: 2px;
|
|
183
|
+
box-shadow: 0 0 24px rgba(255,170,60,.45);
|
|
184
|
+
}
|
|
185
|
+
60% {
|
|
186
|
+
outline: 2px solid rgba(255,170,60,.35);
|
|
187
|
+
outline-offset: 2px;
|
|
188
|
+
box-shadow: 0 0 12px rgba(255,170,60,.15);
|
|
189
|
+
}
|
|
190
|
+
100% {
|
|
191
|
+
outline: 2px solid rgba(255,170,60,0);
|
|
192
|
+
outline-offset: 2px;
|
|
193
|
+
box-shadow: 0 0 0 rgba(255,170,60,0);
|
|
194
|
+
}
|
|
124
195
|
}
|
|
125
196
|
.sr-note {
|
|
126
197
|
display: none;
|
|
@@ -144,4 +215,4 @@
|
|
|
144
215
|
white-space: pre-wrap;
|
|
145
216
|
max-width: min(720px, 90vw);
|
|
146
217
|
}
|
|
147
|
-
`;function
|
|
218
|
+
`;function Vt(o){const n=A.usePlugin(),[f,g]=i.createSignal([]),[j,C]=i.createSignal(""),[E,S]=i.createSignal(!1),[yt,kt]=i.createSignal(""),[Q,tt]=i.createSignal(""),[x,q]=i.createSignal(!1),[et,L]=i.createSignal(0),[rt,R]=i.createSignal(""),v=new Map;let O=0,I=!1,M=!1,J="",_;const P=()=>o.session()?.id,N=()=>{const t=[...v.values()].sort((r,l)=>r.created-l.created),e=i.untrack(f);e.length===t.length&&e.every((r,l)=>r===t[l])||(g(t),xt=t.length)},nt=t=>{const e=v.get(t.id);e&&e.created===t.time&&e.text===t.text||v.set(t.id,{id:t.id,created:t.time,text:t.text})},St=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=++O;I=!0;let l="";try{let s;const c=e==="full"?qt:1,d=e==="full"?Rt:Ot;let p=e!=="full";for(let b=0;b<c;b++){let m;try{m=await St(t,d,s)}catch(h){l=String(h?.message??h);break}if(r!==O)return;p||(v.clear(),p=!0);let u=0;for(const h of m?.data??[])v.has(h.id)||(nt({id:h.id,time:h.time?.created??0,text:gt(h)}),u++);N();const k=m?.cursor?.next;if(!k||u===0)break;s=k}}finally{I=!1,J=t,Z=l,kt(v.size===0?l||"暂无用户消息":""),M&&(M=!1,st())}},st=()=>{_||(_=setTimeout(()=>{_=void 0;const t=i.untrack(P);t&&z(t,"light")},300))},at=t=>{if(!(!t||t!==i.untrack(P))){if(I){M=!0;return}st()}},$t=t=>{const e=t?.type;if(e==="message.updated"){const r=t?.data??{},l=r.info;l?.role==="user"&&l?.id&&(nt({id:l.id,time:l.time?.created??0,text:gt(l)}),N()),at(r.sessionID)}else if(e==="message.removed"){const r=t?.data??{};r.messageID&&v.delete(r.messageID)&&N(),at(r.sessionID)}else if(e==="location.shutdown"){const r=i.untrack(P);r&&J===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 d=await c.next();if(d.done)break;e=0,$t(d.value)}}catch{}try{await c.return?.()}catch{}if(t)break;e++,await bt(Math.min(2e3*e,15e3))}})(),i.onCleanup(()=>{t=!0,clearTimeout(r),_&&clearTimeout(_)})}),i.createEffect(()=>{const t=P();t&&i.untrack(()=>{J!==t&&(O++,I=!1,M=!1,v.clear(),g([]),C(""),H="",z(t,"full"))})});let D;const T=()=>(D?.isConnected||(D=vt()),D),Et=()=>{const t=T();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 s=l.getAttribute("data-message-id");s&&C(s);break}};let F=!1;const ot=()=>{F||(F=!0,requestAnimationFrame(()=>{F=!1,Et()}))};i.onMount(()=>{const t=()=>ot();document.addEventListener("scroll",t,!0);const e=setInterval(ot,1500);i.onCleanup(()=>{document.removeEventListener("scroll",t,!0),clearInterval(e)})});let it=0;const w=(t,e=2600)=>{const r=++it;tt(t),setTimeout(()=>{r===it&&tt("")},e)};i.onMount(()=>{const t=r=>{if(r.key==="Escape"){if(i.untrack(x)){pt(),r.stopPropagation();return}i.untrack(E)&&(S(!1),r.stopPropagation())}},e=r=>{if(!i.untrack(E))return;(r.composedPath?.()??[]).some(c=>c instanceof Element&&c.closest?.(".sr-rail"))||S(!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(`${G} · ${W(n)}`),e=()=>S(r=>!r);window.addEventListener(ht,t),window.addEventListener(Y,e),i.onCleanup(()=>{window.removeEventListener(ht,t),window.removeEventListener(Y,e)}),w(`会话轮次 ${G} 已加载`)});let H="",y=0,lt=0,ct="";const U=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{}},_t=()=>{for(const t of f())if($(t.id))return t},dt=(t,e)=>{Bt(`${t}#${wt}${e}`),H=e},ut=()=>{const e=T()?.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}`},Tt=(t,e,r)=>new Promise(l=>{let s=!1,c,d,p;const b=Date.now(),m=ut(),u=B=>{if(!s){s=!0;try{c?.disconnect()}catch{}clearInterval(d),clearTimeout(p),l(B??null)}},k=B=>{if(e!==y)return u(null);const ft=$(t);if(ft)return u(ft);B&&Date.now()-b>zt&&ut()!==m&&u(null)},h=$(t);if(h)return u(h);c=new MutationObserver(()=>k(!1)),c.observe(document.documentElement,{childList:!0,subtree:!0}),d=setInterval(()=>k(!0),120),p=setTimeout(()=>u($(t)),Math.max(400,r))}),pt=()=>{y++,q(!1),L(0),R(""),w("已取消跳转")},At=async t=>{const e=++y;X=t.id;const r=Date.now();if(t.id===ct&&r-lt<Ft){const d=$(t.id),p=T();if(d&&p){K(p,d),U(d);return}}lt=r,ct=t.id;const l=T(),s=$(t.id);if(s&&l){K(l,s),U(s),w("已定位");return}const c=Ut(n);if(!c){w(`无法读取当前路由,无法定位 · ${W(n)}`);return}q(!0),R(t.id),L(0),S(!0),w("正在定位…",V);try{const d=Date.now()+V;for(let p=0;p<Jt&&Date.now()<d;p++){if(e!==y)return;if(L(p+1),w(`跳转中 · 第 ${p+1} 页`,V),H===t.id){const u=_t()??f()[0];u&&(dt(c,u.id),await bt(120))}if(e!==y)return;dt(c,t.id);const b=Math.min(Nt,d-Date.now()),m=await Tt(t.id,e,b);if(e!==y)return;if(m){const u=T()??l;u&&K(u,m),U(m),w("已定位");return}}e===y&&w(`未能定位 · ${W(n)}`)}finally{e===y&&(q(!1),L(0),R(""))}};i.createEffect(()=>{x()&&rt()&&requestAnimationFrame(()=>{try{document.querySelector('.sr-list .sr-turn[data-busy="true"]')?.scrollIntoView({block:"nearest"})}catch{}})});const Ct=[a.createComponent(i.Show,{get when(){return Q()},get children(){var t=Lt();return a.insert(t,Q),t}}),(()=>{var t=Mt(),e=t.firstChild,r=e.firstChild,l=e.nextSibling;return t.addEventListener("mouseleave",()=>{x()||S(!1)}),t.addEventListener("mouseenter",()=>S(!0)),a.insert(e,a.createComponent(i.Show,{get when(){return x()},get fallback(){return(()=>{var s=Pt();return s.firstChild,a.insert(s,(()=>{var c=a.memo(()=>f().length>0);return()=>c()?` · ${f().length}`:""})(),null),s})()},get children(){var s=It();return s.firstChild,a.insert(s,(()=>{var c=a.memo(()=>et()>0);return()=>c()?` · 第 ${et()} 页`:""})(),null),s}}),r),r.$$click=()=>{x()?pt():S(!1)},a.insert(l,a.createComponent(i.Show,{get when(){return f().length>0},get fallback(){return(()=>{var s=Dt();return a.insert(s,()=>yt()||`… ${G}`),s})()},get children(){return a.createComponent(i.For,{get each(){return f()},children:(s,c)=>(()=>{var d=jt(),p=d.firstChild,b=p.firstChild;b.firstChild;var m=b.nextSibling;return d.$$click=()=>{At(s)},a.insert(b,()=>c()+1,null),a.insert(m,()=>Ht(s.created)),a.insert(p,()=>s.text,null),a.effect(u=>{var k=j()===s.id?"true":"false",h=x()&&rt()===s.id?"true":"false";return k!==u.e&&a.setAttribute(d,"data-active",u.e=k),h!==u.t&&a.setAttribute(d,"data-busy",u.t=h),u},{e:void 0,t:void 0}),d})()})}})),a.effect(s=>{var c=E()?"true":"false",d=x()?"true":"false",p=x()?"取消跳转 (Esc)":"关闭 (Esc)";return c!==s.e&&a.setAttribute(t,"data-open",s.e=c),d!==s.t&&a.setAttribute(t,"data-jumping",s.t=d),p!==s.a&&a.setAttribute(r,"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:Ct})}const Wt=A.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(A.Style,Gt),o.add(A.Slot,{at:"window.bottom",render:()=>a.createComponent(i.Show,{get when(){return o.sessions.current()},get children(){return a.createComponent(Vt,{session:()=>o.sessions.current()})}})}),o.add(A.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(Y))}}))}});a.delegateEvents(["click"]);exports.default=Wt;
|
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 = "v12"
|
|
10
10
|
const HASH_PREFIX = "message-"
|
|
11
11
|
const DIAG_EVENT = "session-turns:diag"
|
|
12
12
|
const OPEN_EVENT = "session-turns:open"
|
|
@@ -17,10 +17,13 @@ const FULL_PAGE_LIMIT = 40
|
|
|
17
17
|
const FULL_PAGE_SIZE = 200
|
|
18
18
|
const LIGHT_PAGE_SIZE = 50
|
|
19
19
|
|
|
20
|
-
// jump loop budgets
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
20
|
+
// jump loop budgets. One round pages in roughly one host history page (~5
|
|
21
|
+
// turns); a round ends as soon as the host appends rows, so the loop is paced
|
|
22
|
+
// by real progress instead of a fixed sleep.
|
|
23
|
+
const JUMP_ROUNDS = 120
|
|
24
|
+
const JUMP_TOTAL_MS = 45000
|
|
25
|
+
const JUMP_ROW_TIMEOUT_MS = 4000
|
|
26
|
+
const JUMP_SETTLE_MS = 300
|
|
24
27
|
const SAME_TARGET_WINDOW_MS = 800
|
|
25
28
|
|
|
26
29
|
// Filled by Rail.load()/jump() so the command palette can report diagnostics.
|
|
@@ -143,50 +146,48 @@ const CSS = `
|
|
|
143
146
|
position: fixed;
|
|
144
147
|
top: 50%;
|
|
145
148
|
transform: translateY(-50%);
|
|
146
|
-
right:
|
|
147
|
-
max-height:
|
|
148
|
-
width:
|
|
149
|
+
right: 14px;
|
|
150
|
+
max-height: 66vh;
|
|
151
|
+
width: 16px;
|
|
149
152
|
display: flex;
|
|
150
153
|
flex-direction: column;
|
|
151
|
-
align-items: flex-end;
|
|
152
154
|
justify-content: center;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
overflow-y: auto;
|
|
156
|
-
overflow-x: hidden;
|
|
157
|
-
scrollbar-width: none;
|
|
158
|
-
/* keep wheel scrolling from chaining into the host timeline when the rail
|
|
159
|
-
reaches its own end */
|
|
160
|
-
overscroll-behavior: contain;
|
|
155
|
+
padding: 6px 2px;
|
|
156
|
+
overflow: hidden;
|
|
161
157
|
z-index: 400;
|
|
162
158
|
border-radius: 10px;
|
|
159
|
+
border: 1px solid transparent;
|
|
163
160
|
color: var(--text-base, currentColor);
|
|
164
161
|
background: transparent;
|
|
165
|
-
transition: width .18s ease, background .18s ease, box-shadow .18s ease;
|
|
162
|
+
transition: width .18s ease, background .18s ease, box-shadow .18s ease, border-color .18s ease;
|
|
166
163
|
}
|
|
167
|
-
.sr-rail::-webkit-scrollbar { display: none; }
|
|
168
164
|
.sr-rail[data-open="true"] {
|
|
169
|
-
width:
|
|
170
|
-
padding: 10px
|
|
165
|
+
width: 340px;
|
|
166
|
+
padding: 10px;
|
|
171
167
|
justify-content: flex-start;
|
|
172
|
-
background: var(--surface-base, rgba(
|
|
173
|
-
backdrop-filter: blur(
|
|
174
|
-
-webkit-backdrop-filter: blur(
|
|
175
|
-
|
|
168
|
+
background: var(--surface-base, rgba(24,24,26,.86));
|
|
169
|
+
backdrop-filter: blur(16px);
|
|
170
|
+
-webkit-backdrop-filter: blur(16px);
|
|
171
|
+
border-color: rgba(127,127,127,.25);
|
|
172
|
+
box-shadow: 0 8px 32px rgba(0,0,0,.28);
|
|
176
173
|
}
|
|
177
174
|
.sr-head {
|
|
178
175
|
display: none;
|
|
179
176
|
align-items: center;
|
|
180
177
|
justify-content: space-between;
|
|
181
178
|
gap: 8px;
|
|
182
|
-
padding: 0 2px
|
|
183
|
-
margin-bottom:
|
|
184
|
-
border-bottom: 1px solid rgba(127,127,127,.
|
|
179
|
+
padding: 0 2px 8px;
|
|
180
|
+
margin-bottom: 6px;
|
|
181
|
+
border-bottom: 1px solid rgba(127,127,127,.2);
|
|
185
182
|
flex-shrink: 0;
|
|
186
|
-
width: 100%;
|
|
187
183
|
}
|
|
188
184
|
.sr-rail[data-open="true"] .sr-head { display: flex; }
|
|
189
|
-
.sr-title { font-size:
|
|
185
|
+
.sr-title { font-size: 12px; opacity: .6; white-space: nowrap; }
|
|
186
|
+
.sr-title.sr-progress {
|
|
187
|
+
opacity: .95;
|
|
188
|
+
color: rgba(255,170,60,.95);
|
|
189
|
+
font-variant-numeric: tabular-nums;
|
|
190
|
+
}
|
|
190
191
|
.sr-close {
|
|
191
192
|
border: 0;
|
|
192
193
|
background: transparent;
|
|
@@ -201,6 +202,26 @@ const CSS = `
|
|
|
201
202
|
flex-shrink: 0;
|
|
202
203
|
}
|
|
203
204
|
.sr-close:hover { opacity: 1; background: var(--surface-base-hover, rgba(127,127,127,.14)); }
|
|
205
|
+
/* the scrollable list; the header stays fixed above it */
|
|
206
|
+
.sr-list {
|
|
207
|
+
flex: 1;
|
|
208
|
+
min-height: 0;
|
|
209
|
+
display: flex;
|
|
210
|
+
flex-direction: column;
|
|
211
|
+
justify-content: center;
|
|
212
|
+
gap: 2px;
|
|
213
|
+
overflow-y: auto;
|
|
214
|
+
overflow-x: hidden;
|
|
215
|
+
scrollbar-width: none;
|
|
216
|
+
}
|
|
217
|
+
.sr-rail[data-open="true"] .sr-list { justify-content: flex-start; gap: 1px; scrollbar-width: thin; }
|
|
218
|
+
.sr-list::-webkit-scrollbar { display: none; }
|
|
219
|
+
.sr-rail[data-open="true"] .sr-list::-webkit-scrollbar { display: block; width: 8px; }
|
|
220
|
+
.sr-list::-webkit-scrollbar-track { background: transparent; }
|
|
221
|
+
.sr-rail[data-open="true"] .sr-list::-webkit-scrollbar-thumb {
|
|
222
|
+
background: rgba(127,127,127,.35);
|
|
223
|
+
border-radius: 4px;
|
|
224
|
+
}
|
|
204
225
|
.sr-turn {
|
|
205
226
|
display: flex;
|
|
206
227
|
align-items: center;
|
|
@@ -216,6 +237,15 @@ const CSS = `
|
|
|
216
237
|
font: inherit;
|
|
217
238
|
border-radius: 4px;
|
|
218
239
|
}
|
|
240
|
+
/* expanded: dash flips to a left-side indicator, content reads left-to-right */
|
|
241
|
+
.sr-rail[data-open="true"] .sr-turn {
|
|
242
|
+
flex-direction: row-reverse;
|
|
243
|
+
justify-content: flex-start;
|
|
244
|
+
gap: 9px;
|
|
245
|
+
padding: 5px 8px;
|
|
246
|
+
min-height: 0;
|
|
247
|
+
border-radius: 6px;
|
|
248
|
+
}
|
|
219
249
|
.sr-turn:focus-visible, .sr-close:focus-visible { outline: 1px solid currentColor; outline-offset: -1px; }
|
|
220
250
|
.sr-dash {
|
|
221
251
|
display: block;
|
|
@@ -225,10 +255,13 @@ const CSS = `
|
|
|
225
255
|
background: currentColor;
|
|
226
256
|
opacity: .28;
|
|
227
257
|
flex-shrink: 0;
|
|
228
|
-
transition: opacity .12s ease, width .12s ease;
|
|
258
|
+
transition: opacity .12s ease, width .12s ease, height .12s ease;
|
|
229
259
|
}
|
|
230
260
|
.sr-turn:hover .sr-dash,
|
|
231
261
|
.sr-turn[data-active="true"] .sr-dash { opacity: .95; width: 18px; }
|
|
262
|
+
.sr-rail[data-open="true"] .sr-dash { width: 3px; height: 16px; }
|
|
263
|
+
.sr-rail[data-open="true"] .sr-turn:hover .sr-dash,
|
|
264
|
+
.sr-rail[data-open="true"] .sr-turn[data-active="true"] .sr-dash { width: 3px; height: 22px; }
|
|
232
265
|
.sr-text {
|
|
233
266
|
display: none;
|
|
234
267
|
margin-right: 10px;
|
|
@@ -244,22 +277,63 @@ const CSS = `
|
|
|
244
277
|
opacity: .45;
|
|
245
278
|
background: transparent;
|
|
246
279
|
}
|
|
247
|
-
.sr-rail[data-open="true"] .sr-text {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
280
|
+
.sr-rail[data-open="true"] .sr-text {
|
|
281
|
+
display: -webkit-box;
|
|
282
|
+
-webkit-line-clamp: 2;
|
|
283
|
+
-webkit-box-orient: vertical;
|
|
284
|
+
overflow: hidden;
|
|
285
|
+
flex: 1;
|
|
286
|
+
min-width: 0;
|
|
287
|
+
margin: 0;
|
|
288
|
+
padding: 0;
|
|
289
|
+
text-align: left;
|
|
290
|
+
white-space: normal;
|
|
291
|
+
line-height: 1.45;
|
|
292
|
+
max-width: none;
|
|
293
|
+
opacity: .8;
|
|
294
|
+
}
|
|
295
|
+
.sr-num, .sr-time {
|
|
296
|
+
opacity: .55;
|
|
297
|
+
margin-right: 6px;
|
|
298
|
+
font-variant-numeric: tabular-nums;
|
|
252
299
|
}
|
|
253
|
-
.sr-turn
|
|
300
|
+
.sr-turn:hover { background: var(--surface-base-hover, rgba(127,127,127,.14)); }
|
|
301
|
+
.sr-rail[data-open="true"] .sr-turn:hover .sr-text,
|
|
302
|
+
.sr-rail[data-open="true"] .sr-turn[data-active="true"] .sr-text { opacity: 1; }
|
|
303
|
+
.sr-turn[data-active="true"] { font-weight: 500; }
|
|
304
|
+
/* the turn currently being jumped to */
|
|
305
|
+
.sr-turn[data-busy="true"] { background: rgba(255,170,60,.12); }
|
|
306
|
+
.sr-turn[data-busy="true"] .sr-dash {
|
|
254
307
|
opacity: 1;
|
|
255
|
-
|
|
256
|
-
|
|
308
|
+
width: 3px;
|
|
309
|
+
height: 22px;
|
|
310
|
+
background: rgba(255,170,60,.95);
|
|
311
|
+
animation: srBusy 1s ease-in-out infinite;
|
|
312
|
+
}
|
|
313
|
+
.sr-turn[data-busy="false"] .sr-dash { animation: none; }
|
|
314
|
+
@keyframes srBusy {
|
|
315
|
+
0%, 100% { opacity: .35; }
|
|
316
|
+
50% { opacity: 1; }
|
|
257
317
|
}
|
|
258
|
-
/*
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
318
|
+
/* pulse a landed bubble: outline follows the bubble's own border-radius, so no
|
|
319
|
+
shape is imposed and the message background stays untouched */
|
|
320
|
+
.sr-flash-ring { animation: srRing 1.8s ease-out 1; }
|
|
321
|
+
@keyframes srRing {
|
|
322
|
+
0% {
|
|
323
|
+
outline: 2px solid rgba(255,170,60,.95);
|
|
324
|
+
outline-offset: 2px;
|
|
325
|
+
box-shadow: 0 0 24px rgba(255,170,60,.45);
|
|
326
|
+
}
|
|
327
|
+
60% {
|
|
328
|
+
outline: 2px solid rgba(255,170,60,.35);
|
|
329
|
+
outline-offset: 2px;
|
|
330
|
+
box-shadow: 0 0 12px rgba(255,170,60,.15);
|
|
331
|
+
}
|
|
332
|
+
100% {
|
|
333
|
+
outline: 2px solid rgba(255,170,60,0);
|
|
334
|
+
outline-offset: 2px;
|
|
335
|
+
box-shadow: 0 0 0 rgba(255,170,60,0);
|
|
336
|
+
}
|
|
263
337
|
}
|
|
264
338
|
.sr-note {
|
|
265
339
|
display: none;
|
|
@@ -296,6 +370,11 @@ function Rail(props) {
|
|
|
296
370
|
const [open, setOpen] = createSignal(false)
|
|
297
371
|
const [note, setNote] = createSignal("")
|
|
298
372
|
const [status, setStatus] = createSignal("")
|
|
373
|
+
// Jump progress. While `jumping` is true the panel stays pinned open and the
|
|
374
|
+
// header reports the page being loaded; `busyID` marks the target row.
|
|
375
|
+
const [jumping, setJumping] = createSignal(false)
|
|
376
|
+
const [jumpPage, setJumpPage] = createSignal(0)
|
|
377
|
+
const [busyID, setBusyID] = createSignal("")
|
|
299
378
|
|
|
300
379
|
// ---- turn cache ---------------------------------------------------------
|
|
301
380
|
// Object identity is kept stable across refreshes so Solid's <For> reuses
|
|
@@ -550,6 +629,13 @@ function Rail(props) {
|
|
|
550
629
|
onMount(() => {
|
|
551
630
|
const onKey = (event) => {
|
|
552
631
|
if (event.key !== "Escape") return
|
|
632
|
+
// During a long jump Esc cancels the jump first and keeps the panel open,
|
|
633
|
+
// so the progress display doesn't vanish mid-flight.
|
|
634
|
+
if (untrack(jumping)) {
|
|
635
|
+
cancelJump()
|
|
636
|
+
event.stopPropagation()
|
|
637
|
+
return
|
|
638
|
+
}
|
|
553
639
|
if (untrack(open)) {
|
|
554
640
|
setOpen(false)
|
|
555
641
|
// Only swallow Esc while the rail is actually open, so the host keeps
|
|
@@ -594,14 +680,21 @@ function Rail(props) {
|
|
|
594
680
|
let lastJumpAt = 0
|
|
595
681
|
let lastJumpTurnID = ""
|
|
596
682
|
|
|
597
|
-
// Brief highlight on the
|
|
598
|
-
//
|
|
683
|
+
// Brief highlight on the message bubble. The timeline row spans the whole
|
|
684
|
+
// width, so tinting it looks like a stray orange band. The visible bubble is
|
|
685
|
+
// `[data-slot="user-message-text"]` (background + 10px radius); its parent
|
|
686
|
+
// `user-message-body` is only a layout column. An outline + glow hugs the
|
|
687
|
+
// bubble's own shape without altering its background.
|
|
599
688
|
const pulse = (row) => {
|
|
600
689
|
try {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
690
|
+
const target =
|
|
691
|
+
row.querySelector('[data-slot="user-message-text"]') ??
|
|
692
|
+
row.querySelector('[data-slot="user-message-body"]') ??
|
|
693
|
+
row
|
|
694
|
+
target.classList.remove("sr-flash-ring")
|
|
695
|
+
void target.offsetWidth // restart the animation when re-pulsing the same node
|
|
696
|
+
target.classList.add("sr-flash-ring")
|
|
697
|
+
setTimeout(() => target.classList.remove("sr-flash-ring"), 1900)
|
|
605
698
|
} catch {}
|
|
606
699
|
}
|
|
607
700
|
|
|
@@ -617,14 +710,29 @@ function Rail(props) {
|
|
|
617
710
|
lastNavID = id
|
|
618
711
|
}
|
|
619
712
|
|
|
713
|
+
// Cheap fingerprint of the mounted timeline rows. Paging in older history
|
|
714
|
+
// prepends rows, so a changed fingerprint means the host did real work and
|
|
715
|
+
// the next hash toggle can be sent immediately.
|
|
716
|
+
const rowSignature = () => {
|
|
717
|
+
const sc = scroller()
|
|
718
|
+
const rows = sc?.querySelectorAll("[data-message-id]") ?? []
|
|
719
|
+
if (rows.length === 0) return "0"
|
|
720
|
+
const first = rows[0].getAttribute("data-message-id") ?? ""
|
|
721
|
+
const last = rows[rows.length - 1].getAttribute("data-message-id") ?? ""
|
|
722
|
+
return `${rows.length}:${first}:${last}`
|
|
723
|
+
}
|
|
724
|
+
|
|
620
725
|
// Resolves when the row enters the DOM (MutationObserver, with a cheap poll
|
|
621
|
-
// as a belt-and-braces fallback), or null on timeout / supersession
|
|
726
|
+
// as a belt-and-braces fallback), or null on timeout / supersession / when
|
|
727
|
+
// the host has paged in another slice (the caller then drives the next seek).
|
|
622
728
|
const waitForRow = (messageID, token, timeout) =>
|
|
623
729
|
new Promise((resolve) => {
|
|
624
730
|
let done = false
|
|
625
731
|
let observer
|
|
626
732
|
let poll
|
|
627
733
|
let timer
|
|
734
|
+
const started = Date.now()
|
|
735
|
+
const baseline = rowSignature()
|
|
628
736
|
const finish = (row) => {
|
|
629
737
|
if (done) return
|
|
630
738
|
done = true
|
|
@@ -635,19 +743,31 @@ function Rail(props) {
|
|
|
635
743
|
clearTimeout(timer)
|
|
636
744
|
resolve(row ?? null)
|
|
637
745
|
}
|
|
638
|
-
const check = () => {
|
|
746
|
+
const check = (full) => {
|
|
639
747
|
if (token !== jumpToken) return finish(null)
|
|
640
748
|
const row = findRow(messageID)
|
|
641
|
-
if (row) finish(row)
|
|
749
|
+
if (row) return finish(row)
|
|
750
|
+
// Let the click settle before trusting a DOM change as "a page arrived".
|
|
751
|
+
// Only the poll evaluates the row fingerprint: streaming output fires
|
|
752
|
+
// the observer constantly and a full query on every mutation is wasted.
|
|
753
|
+
if (full && Date.now() - started > JUMP_SETTLE_MS && rowSignature() !== baseline) finish(null)
|
|
642
754
|
}
|
|
643
755
|
const existing = findRow(messageID)
|
|
644
756
|
if (existing) return finish(existing)
|
|
645
|
-
observer = new MutationObserver(check)
|
|
757
|
+
observer = new MutationObserver(() => check(false))
|
|
646
758
|
observer.observe(document.documentElement, { childList: true, subtree: true })
|
|
647
|
-
poll = setInterval(check, 120)
|
|
759
|
+
poll = setInterval(() => check(true), 120)
|
|
648
760
|
timer = setTimeout(() => finish(findRow(messageID)), Math.max(400, timeout))
|
|
649
761
|
})
|
|
650
762
|
|
|
763
|
+
const cancelJump = () => {
|
|
764
|
+
jumpToken++
|
|
765
|
+
setJumping(false)
|
|
766
|
+
setJumpPage(0)
|
|
767
|
+
setBusyID("")
|
|
768
|
+
flash("已取消跳转")
|
|
769
|
+
}
|
|
770
|
+
|
|
651
771
|
const jump = async (turn) => {
|
|
652
772
|
const token = ++jumpToken
|
|
653
773
|
lastJumpID = turn.id
|
|
@@ -684,40 +804,72 @@ function Rail(props) {
|
|
|
684
804
|
return
|
|
685
805
|
}
|
|
686
806
|
|
|
687
|
-
//
|
|
688
|
-
// idle. Drive it: each round toggles the hash to an
|
|
689
|
-
// back to the target, so the cursor machine runs
|
|
690
|
-
// A
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
807
|
+
// Deep history: the host only pages in ~one history page (~5 turns) per
|
|
808
|
+
// seek and then goes idle. Drive it: each round toggles the hash to an
|
|
809
|
+
// already-loaded turn and back to the target, so the cursor machine runs
|
|
810
|
+
// another seek/page. A round ends as soon as rows appear instead of always
|
|
811
|
+
// sleeping 6s. A newer click bumps jumpToken, which cancels this loop at
|
|
812
|
+
// every await; Esc cancels it too.
|
|
813
|
+
setJumping(true)
|
|
814
|
+
setBusyID(turn.id)
|
|
815
|
+
setJumpPage(0)
|
|
816
|
+
// Pin the panel open even if the pointer has left it meanwhile.
|
|
817
|
+
setOpen(true)
|
|
818
|
+
flash("正在定位…", JUMP_TOTAL_MS)
|
|
819
|
+
try {
|
|
820
|
+
const deadline = Date.now() + JUMP_TOTAL_MS
|
|
821
|
+
for (let round = 0; round < JUMP_ROUNDS && Date.now() < deadline; round++) {
|
|
822
|
+
if (token !== jumpToken) return
|
|
823
|
+
setJumpPage(round + 1)
|
|
824
|
+
flash(`跳转中 · 第 ${round + 1} 页`, JUMP_TOTAL_MS)
|
|
825
|
+
|
|
826
|
+
if (lastNavID === turn.id) {
|
|
827
|
+
const other = oldestLoadedTurn() ?? turns()[0]
|
|
828
|
+
if (other) {
|
|
829
|
+
navTo(path, other.id)
|
|
830
|
+
await sleep(120)
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (token !== jumpToken) return
|
|
834
|
+
navTo(path, turn.id)
|
|
835
|
+
|
|
836
|
+
const remaining = Math.min(JUMP_ROW_TIMEOUT_MS, deadline - Date.now())
|
|
837
|
+
const landed = await waitForRow(turn.id, token, remaining)
|
|
838
|
+
if (token !== jumpToken) return
|
|
839
|
+
if (landed) {
|
|
840
|
+
const live = scroller() ?? sc
|
|
841
|
+
if (live) scrollToRow(live, landed)
|
|
842
|
+
pulse(landed)
|
|
843
|
+
flash("已定位")
|
|
844
|
+
return
|
|
701
845
|
}
|
|
702
846
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (live) scrollToRow(live, landed)
|
|
712
|
-
pulse(landed)
|
|
713
|
-
flash("已定位")
|
|
714
|
-
return
|
|
847
|
+
|
|
848
|
+
if (token === jumpToken) flash(`未能定位 · ${diagnostics(ctx)}`)
|
|
849
|
+
} finally {
|
|
850
|
+
// A newer jump owns the signals when the token moved on.
|
|
851
|
+
if (token === jumpToken) {
|
|
852
|
+
setJumping(false)
|
|
853
|
+
setJumpPage(0)
|
|
854
|
+
setBusyID("")
|
|
715
855
|
}
|
|
716
856
|
}
|
|
717
|
-
|
|
718
|
-
if (token === jumpToken) flash(`未能定位 · ${diagnostics(ctx)}`)
|
|
719
857
|
}
|
|
720
858
|
|
|
859
|
+
// Keep the busy row visible in the panel list while the jump pages through
|
|
860
|
+
// history, so the "which turn am I waiting for" answer stays on screen.
|
|
861
|
+
createEffect(() => {
|
|
862
|
+
if (!jumping()) return
|
|
863
|
+
if (!busyID()) return
|
|
864
|
+
requestAnimationFrame(() => {
|
|
865
|
+
try {
|
|
866
|
+
document
|
|
867
|
+
.querySelector('.sr-list .sr-turn[data-busy="true"]')
|
|
868
|
+
?.scrollIntoView({ block: "nearest" })
|
|
869
|
+
} catch {}
|
|
870
|
+
})
|
|
871
|
+
})
|
|
872
|
+
|
|
721
873
|
const rail = (
|
|
722
874
|
<>
|
|
723
875
|
<Show when={status()}>
|
|
@@ -726,33 +878,60 @@ function Rail(props) {
|
|
|
726
878
|
<div
|
|
727
879
|
class="sr-rail"
|
|
728
880
|
data-open={open() ? "true" : "false"}
|
|
881
|
+
data-jumping={jumping() ? "true" : "false"}
|
|
729
882
|
onMouseEnter={() => setOpen(true)}
|
|
730
|
-
|
|
883
|
+
// Keep the panel pinned while a long history jump runs; otherwise the
|
|
884
|
+
// mouse drifting off the rail hides exactly the progress the user is
|
|
885
|
+
// waiting for. Esc / × still close (or cancel) it.
|
|
886
|
+
onMouseLeave={() => {
|
|
887
|
+
if (!jumping()) setOpen(false)
|
|
888
|
+
}}
|
|
731
889
|
>
|
|
732
890
|
<div class="sr-head">
|
|
733
|
-
<
|
|
734
|
-
|
|
891
|
+
<Show
|
|
892
|
+
when={jumping()}
|
|
893
|
+
fallback={
|
|
894
|
+
<span class="sr-title">会话轮次{turns().length > 0 ? ` · ${turns().length}` : ""}</span>
|
|
895
|
+
}
|
|
896
|
+
>
|
|
897
|
+
<span class="sr-title sr-progress">
|
|
898
|
+
跳转中{jumpPage() > 0 ? ` · 第 ${jumpPage()} 页` : ""}
|
|
899
|
+
</span>
|
|
900
|
+
</Show>
|
|
901
|
+
<button
|
|
902
|
+
type="button"
|
|
903
|
+
class="sr-close"
|
|
904
|
+
title={jumping() ? "取消跳转 (Esc)" : "关闭 (Esc)"}
|
|
905
|
+
onClick={() => {
|
|
906
|
+
if (jumping()) cancelJump()
|
|
907
|
+
else setOpen(false)
|
|
908
|
+
}}
|
|
909
|
+
>
|
|
735
910
|
×
|
|
736
911
|
</button>
|
|
737
912
|
</div>
|
|
738
|
-
<
|
|
739
|
-
<
|
|
740
|
-
{(
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
913
|
+
<div class="sr-list">
|
|
914
|
+
<Show when={turns().length > 0} fallback={<div class="sr-note">{note() || `… ${VERSION}`}</div>}>
|
|
915
|
+
<For each={turns()}>
|
|
916
|
+
{(turn, index) => (
|
|
917
|
+
<button
|
|
918
|
+
type="button"
|
|
919
|
+
class="sr-turn"
|
|
920
|
+
data-active={active() === turn.id ? "true" : "false"}
|
|
921
|
+
data-busy={jumping() && busyID() === turn.id ? "true" : "false"}
|
|
922
|
+
onClick={() => void jump(turn)}
|
|
923
|
+
>
|
|
924
|
+
<span class="sr-text">
|
|
925
|
+
<span class="sr-num">#{index() + 1}</span>
|
|
926
|
+
<span class="sr-time">{formatTime(turn.created)}</span>
|
|
927
|
+
{turn.text}
|
|
928
|
+
</span>
|
|
929
|
+
<span class="sr-dash" />
|
|
930
|
+
</button>
|
|
931
|
+
)}
|
|
932
|
+
</For>
|
|
933
|
+
</Show>
|
|
934
|
+
</div>
|
|
756
935
|
</div>
|
|
757
936
|
</>
|
|
758
937
|
)
|