@max-null/dsh-chat-rail 0.6.0 → 0.6.2

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/lib/client.js CHANGED
@@ -15,12 +15,11 @@ window.__ModuleLoader__.load({
15
15
  *
16
16
  * Two deliberate fixes over the dsh-chat-timeline reference:
17
17
  *
18
- * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout
19
- * push through the `--dsh-sidebar-width` CSS variable on `:root` (the
20
- * `#root` margin-right transition reads the same variable). The rail's
21
- * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same
22
- * transition timing, so rail and scrollport move together while the panel
23
- * expands — no "panel first, scrollbar later" lag.
18
+ * 1. POSITION ANCHOR. The rail's right edge follows the conversation column
19
+ * (`[data-conversation-scroll]`), not the viewport: the official right
20
+ * sidebar pushes the conversation narrower, and the rail must travel with
21
+ * it. `--dsh-sidebar-width` reads 0px on the current kernel, so a CSS-only
22
+ * offset would pin the rail underneath that sidebar.
24
23
  *
25
24
  * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)
26
25
  * lines — nearly invisible on a light background. Here the rail keeps a
@@ -94,19 +93,20 @@ window.__ModuleLoader__.load({
94
93
  return `${d.getMonth() + 1}/${d.getDate()}`;
95
94
  }
96
95
  const css = [
97
- ".crl_nav{user-select:none;z-index:100;position:fixed;right:calc(var(--dsh-sidebar-width,0px) + 3px);top:calc((100vh - var(--dsh-sidebar-height,0px)) / 2);transform:translateY(-50%);width:36px;max-height:min(60vh,420px,calc(100vh - var(--dsh-sidebar-height,0px) - 40px));display:flex;flex-direction:column;align-items:center;box-sizing:border-box;padding:10px 0;border-radius:18px;overflow-y:hidden;overflow-x:hidden;background:rgba(255,255,255,.55);border:1px solid rgba(0,0,0,.07);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.18) transparent;transition:width .25s cubic-bezier(.4,0,.2,1),right var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),top var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),background .2s ease,border-color .2s ease,box-shadow .2s ease}",
98
- "body[data-ds-dark-theme] .crl_nav,[data-theme='dark'] .crl_nav,.dark .crl_nav{background:rgba(28,28,32,.6);border-color:rgba(255,255,255,.09);scrollbar-color:rgba(255,255,255,.25) transparent}",
99
- ".crl_nav.crl_show{width:280px;overflow-y:auto;align-items:stretch;background:rgba(255,255,255,.94);border-color:rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(0,0,0,.10),0 2px 8px rgba(0,0,0,.05)}",
96
+ ".crl_nav{user-select:none;z-index:100;position:fixed;right:3px;top:calc((100vh - var(--dsh-sidebar-height,0px)) / 2);transform:translateY(-50%);width:36px;max-height:min(60vh,420px,calc(100vh - var(--dsh-sidebar-height,0px) - 40px));display:flex;flex-direction:column;align-items:center;box-sizing:border-box;padding:0 0 10px;border-radius:18px;overflow-y:hidden;overflow-x:hidden;background:rgba(255,255,255,.55);border:1px solid rgba(0,0,0,.07);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);scrollbar-width:none;transition:width .25s cubic-bezier(.4,0,.2,1),right .25s cubic-bezier(.4,0,.2,1),top .25s cubic-bezier(.4,0,.2,1),background .2s ease,border-color .2s ease,box-shadow .2s ease}",
97
+ ".crl_list{display:flex;flex-direction:column;align-items:center;width:100%;min-height:0;flex:1 1 auto;position:relative;overflow-y:hidden;overflow-x:hidden;scrollbar-width:none}",
98
+ "body[data-ds-dark-theme] .crl_nav,[data-theme='dark'] .crl_nav,.dark .crl_nav{background:rgba(28,28,32,.6);border-color:rgba(255,255,255,.09)}",
99
+ ".crl_nav.crl_show{width:280px;align-items:stretch;background:rgba(255,255,255,.94);border-color:rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(0,0,0,.10),0 2px 8px rgba(0,0,0,.05)}",
100
100
  "body[data-ds-dark-theme] .crl_nav.crl_show,[data-theme='dark'] .crl_nav.crl_show,.dark .crl_nav.crl_show{background:rgba(28,28,32,.96);border-color:rgba(255,255,255,.09);box-shadow:0 10px 30px rgba(0,0,0,.5),0 2px 8px rgba(0,0,0,.28)}",
101
- ".crl_nav::-webkit-scrollbar{width:4px}",
102
- ".crl_nav::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);border-radius:4px}",
101
+ ".crl_nav::-webkit-scrollbar,.crl_list::-webkit-scrollbar{width:0;height:0}",
102
+ ".crl_show .crl_list{overflow-y:auto;align-items:stretch}",
103
103
  ".crl_loading{position:sticky;top:0;z-index:2;flex-shrink:0;width:100%;padding:4px 0;font-size:10px;line-height:1;text-align:center;color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.5)));background:inherit;pointer-events:none}",
104
104
  ".crl_loading::before{content:\"\";display:inline-block;width:8px;height:8px;margin-right:4px;border:1.5px solid var(--dsw-alias-state-business-primary,#4d6bfe);border-top-color:transparent;border-radius:50%;vertical-align:-1px;animation:crl-spin .8s linear infinite}",
105
105
  ".crl_loading .crl_loadingLabel{display:none}",
106
106
  ".crl_show .crl_loading .crl_loadingLabel{display:inline}",
107
107
  "@keyframes crl-spin{to{transform:rotate(360deg)}}",
108
- ".crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:flex-end;align-items:center;width:100%;box-sizing:border-box;padding:0 9px;line-height:20px;background:none;border:none;font:inherit;text-align:left;color:rgba(0,0,0,.68);transition:padding .25s cubic-bezier(.4,0,.2,1),color .15s ease}",
109
- ".crl_show .crl_item{padding:0 14px}",
108
+ ".crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:center;align-items:center;width:100%;box-sizing:border-box;padding:0 9px;line-height:20px;background:none;border:none;font:inherit;text-align:left;color:rgba(0,0,0,.68);transition:padding .25s cubic-bezier(.4,0,.2,1),color .15s ease}",
109
+ ".crl_show .crl_item{justify-content:flex-end;padding:0 14px}",
110
110
  ".crl_item:hover{color:rgba(0,0,0,.95)}",
111
111
  ".crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
112
112
  "body[data-ds-dark-theme] .crl_item,[data-theme='dark'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}",
@@ -132,7 +132,12 @@ window.__ModuleLoader__.load({
132
132
  "body[data-ds-dark-theme] .crl_line,[data-theme='dark'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}",
133
133
  "body[data-ds-dark-theme] .crl_item:hover .crl_line,[data-theme='dark'] .crl_item:hover .crl_line,.dark .crl_item:hover .crl_line{background-color:rgba(255,255,255,.95)}",
134
134
  "body[data-ds-dark-theme] .crl_item.crl_active .crl_line,[data-theme='dark'] .crl_item.crl_active .crl_line,.dark .crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
135
- ".crl_tip{position:fixed;z-index:200;max-width:360px;max-height:70vh;overflow-y:auto;padding:10px 12px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary,var(--text-primary,rgba(0,0,0,.85)));background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(255,255,255,.97)));border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)));border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16);white-space:pre-wrap;word-break:break-word;pointer-events:none}",
135
+ ".crl_tip{position:fixed;z-index:200;max-width:360px;max-height:70vh;overflow-y:auto;padding:10px 12px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary,var(--text-primary,rgba(0,0,0,.85)));background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(255,255,255,.97)));border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)));border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16);white-space:pre-wrap;word-break:break-word;pointer-events:auto}",
136
+ ".crl_tipQaWrap{margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));display:flex;flex-direction:column;gap:4px;pointer-events:auto}",
137
+ ".crl_tipQa{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;padding:5px 8px;border:none;border-radius:7px;background:rgba(77,107,254,.08);color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background .15s ease}",
138
+ ".crl_tipQa:hover{background:rgba(77,107,254,.16)}",
139
+ ".crl_tipQaIcon{flex:none;display:block;color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
140
+ ".crl_tipQaText{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
136
141
  "body[data-ds-dark-theme] .crl_tip,[data-theme='dark'] .crl_tip,.dark .crl_tip{background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(28,28,32,.97)));border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}",
137
142
  ".crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}",
138
143
  ".crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}",
@@ -156,14 +161,16 @@ window.__ModuleLoader__.load({
156
161
  ".crl_item.crl_favItem .crl_line{background-color:#ffd166}",
157
162
  "body[data-ds-dark-theme] .crl_item.crl_favItem .crl_line,[data-theme='dark'] .crl_item.crl_favItem .crl_line,.dark .crl_item.crl_favItem .crl_line{background-color:#ffd166}",
158
163
  ".crl_item.crl_favItem.crl_active .crl_line{background-color:#ffd166;box-shadow:0 0 6px #ffd166}",
159
- ".crl_favToggle{position:sticky;top:-10px;z-index:3;flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:6px;width:26px;height:26px;margin:0 0 6px;padding:0;border:none;border-radius:13px;background:inherit;color:rgba(0,0,0,.4);cursor:pointer;font-size:11px;line-height:1;white-space:nowrap;transition:background .15s ease,color .15s ease}",
164
+ ".crl_favToggle{flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:6px;width:34px;height:26px;margin:0 0 6px;padding:0;border:none;border-radius:0;background:transparent;border-bottom:1px solid rgba(0,0,0,.07);color:rgba(0,0,0,.42);cursor:pointer;font-size:11px;line-height:1;white-space:nowrap;transition:background .15s ease,color .15s ease,margin .25s cubic-bezier(.4,0,.2,1),left .25s cubic-bezier(.4,0,.2,1)}",
160
165
  ".crl_favToggle:hover{background:rgba(0,0,0,.07);color:rgba(0,0,0,.75)}",
161
166
  ".crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.14)}",
162
- ".crl_show .crl_favToggle{width:auto;height:26px;margin:0 6px 8px;padding:0 11px;border-radius:13px;align-self:flex-start;text-align:left}",
167
+ ".crl_show .crl_favToggle{width:100%;height:28px;margin:0 0 6px;padding:0 12px;border-radius:0;justify-content:flex-start;text-align:left;border-bottom:1px solid rgba(0,0,0,.07)}",
163
168
  ".crl_favToggleLabel{display:none}",
164
169
  ".crl_show .crl_favToggleLabel{display:inline-block;min-width:0;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}",
165
- "body[data-ds-dark-theme] .crl_favToggle,[data-theme='dark'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4)}",
170
+ "body[data-ds-dark-theme] .crl_favToggle,[data-theme='dark'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4);border-bottom-color:rgba(255,255,255,.1)}",
166
171
  "body[data-ds-dark-theme] .crl_favToggle:hover,[data-theme='dark'] .crl_favToggle:hover,.dark .crl_favToggle:hover{background:rgba(255,255,255,.1);color:rgba(255,255,255,.85)}",
172
+ "body[data-ds-dark-theme] .crl_show .crl_favToggle,[data-theme='dark'] .crl_show .crl_favToggle,.dark .crl_show .crl_favToggle{border-bottom-color:rgba(255,255,255,.1)}",
173
+ ".crl_nav.crl_navHidden{opacity:0 !important;pointer-events:none !important}",
167
174
  "body[data-ds-dark-theme] .crl_favToggle.crl_on,[data-theme='dark'] .crl_favToggle.crl_on,.dark .crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.18)}",
168
175
  "@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}",
169
176
  ".crlSetCard{list-style:none;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s;cursor:pointer}",
@@ -190,6 +197,14 @@ window.__ModuleLoader__.load({
190
197
  * CSS module 类名(如 hkplfa_slot,hash 随构建内容漂移)。 */
191
198
  const HIDE_OFFICIAL_CSS = "nav[aria-label=\"轮次导航\"],nav[aria-label=\"Turn navigation\"]{display:none !important}";
192
199
  const HIDE_OFFICIAL_STYLE_ID = "data-crl-hide-official";
200
+ /**
201
+ * `data-plugin` 归属标记:DSH 的 client 模块系统只把**没有** `data-plugin` 的 `<style>`
202
+ * 认领给正在 materialize 的模块(`claimStyles`),而 HMR 重载时按
203
+ * `data-plugin === 模块 id` 逐字匹配删除。裸注入的 style 会被任意模块认领走,
204
+ * 随后随那个模块的一次重载被物理删除——元素还在、样式消失(悬浮球样式事故的机制)。
205
+ * 这里用一个独立于模块 id 的稳定值:既不进别人的账,也不随本模块的进出被清掉。
206
+ */
207
+ const HIDE_OFFICIAL_PLUGIN_TAG = "dsh-chat-rail-hide-official";
193
208
  /** 按设置同步「屏蔽官方 TurnNavigator」样式(showOfficial=true=官方模式:不屏蔽)。 */
194
209
  function syncOfficialHide(showOfficial) {
195
210
  const el = document.head.querySelector(`style[${HIDE_OFFICIAL_STYLE_ID}]`);
@@ -200,10 +215,19 @@ window.__ModuleLoader__.load({
200
215
  if (el === null) {
201
216
  const tag = document.createElement("style");
202
217
  tag.setAttribute(HIDE_OFFICIAL_STYLE_ID, "");
218
+ tag.setAttribute("data-plugin", HIDE_OFFICIAL_PLUGIN_TAG);
203
219
  tag.textContent = HIDE_OFFICIAL_CSS;
204
220
  document.head.appendChild(tag);
205
- }
221
+ } else if (el.textContent !== HIDE_OFFICIAL_CSS) el.textContent = HIDE_OFFICIAL_CSS;
206
222
  }
223
+ /**
224
+ * 会话区剩余宽度低于此值时隐藏导航条(px)。
225
+ *
226
+ * 36px 是 rail 自身宽度,留 ~220px 给消息区——再窄时指示点已无参考意义。
227
+ */
228
+ const RAIL_MIN_SPACE = 260;
229
+ /** 导航条隐藏类(空间不足时由 React 加上;用类而非内联 display,避免与官方模式冲突)。 */
230
+ const NAV_HIDDEN_CLASS = "crl_navHidden";
207
231
  /** 移除所有已注入的行内收藏/填充按钮(官方模式下的回溯清理)。 */
208
232
  function clearRowActions() {
209
233
  for (const row of document.querySelectorAll("[data-crl-actions=\"1\"]")) {
@@ -220,16 +244,20 @@ window.__ModuleLoader__.load({
220
244
  /** No-op subscribe for useSyncExternalStore when the scope is not yet bound. */
221
245
  const NOOP_SUB = (() => () => {});
222
246
  const STYLE_ID = "@max-null/dsh-chat-rail/styles.module.css";
223
- if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`) === null) {
224
- const tag = document.createElement("style");
225
- tag.dataset.plugin = "dsh-chat-rail";
226
- tag.dataset.pluginCss = STYLE_ID;
227
- tag.textContent = css;
228
- document.head.appendChild(tag);
247
+ if (typeof document !== "undefined") {
248
+ const existing = document.head.querySelector(`style[data-plugin-css="${STYLE_ID}"]`);
249
+ if (existing === null) {
250
+ const tag = document.createElement("style");
251
+ tag.dataset.plugin = "dsh-chat-rail";
252
+ tag.dataset.pluginCss = STYLE_ID;
253
+ tag.textContent = css;
254
+ document.head.appendChild(tag);
255
+ } else if (existing.textContent !== css) existing.textContent = css;
229
256
  }
230
257
  const S = {
231
258
  nav: "crl_nav",
232
259
  navShow: "crl_show",
260
+ list: "crl_list",
233
261
  item: "crl_item",
234
262
  itemActive: "crl_active",
235
263
  title: "crl_title",
@@ -241,6 +269,10 @@ window.__ModuleLoader__.load({
241
269
  loading: "crl_loading",
242
270
  loadingLabel: "crl_loadingLabel",
243
271
  tip: "crl_tip",
272
+ tipQaWrap: "crl_tipQaWrap",
273
+ tipQa: "crl_tipQa",
274
+ tipQaIcon: "crl_tipQaIcon",
275
+ tipQaText: "crl_tipQaText",
244
276
  tipImgs: "crl_tipImgs",
245
277
  tipImgWrap: "crl_tipImgWrap",
246
278
  tipImg: "crl_tipImg",
@@ -612,11 +644,89 @@ window.__ModuleLoader__.load({
612
644
  out.sort((a, b) => a.seq - b.seq);
613
645
  return out;
614
646
  }
615
- /** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */
647
+ /** 任一数据平面的节点集合(与 collectFromNodes 同一套取值规则)。 */
648
+ function nodeValuesOf(snapshot) {
649
+ const store = snapshot?.nodes;
650
+ if (store !== void 0 && typeof store.values === "function") return [...store.values()];
651
+ const chat = snapshot?.chat;
652
+ if (chat?.nodes === void 0) return [];
653
+ return [...chat.nodes.values()];
654
+ }
655
+ /**
656
+ * 「提问&回答」也要能被导航。
657
+ *
658
+ * 它**不是** chat node:`ask_user_question` 是助手消息里的一个 tool-call 块,
659
+ * 用户的答案落在同一个 callId 的 `tool/result`(实测会话日志:有 `tool/call
660
+ * name=ask_user_question` 与对应结果,**没有任何带问答痕迹的 user/message**)。
661
+ * 所以 `railMessageOfNode` 的 `kind==='user'` 判定永远覆盖不到它。
662
+ *
663
+ * 但官方给工具调用也设了 DOM 锚点(`ui-tool/ToolCallTree.tsx:36` →
664
+ * `data-chat-anchor-key={`call:${callId}`}`),所以**可跳转**——只要把
665
+ * 助手节点里的问答块收集成 rail 条目即可。
666
+ *
667
+ * 范围:只覆盖已加载的 chat node(不做 loadThrough 拉取未加载历史)——
668
+ * 用户 2026-09-14 拍板。
669
+ *
670
+ * @param snapshot - 当前 chat node 快照(任一平面)。
671
+ * @returns 按 seq 升序的问答条目。
672
+ */
673
+ function collectQaFromNodes(snapshot) {
674
+ const out = [];
675
+ for (const node of nodeValuesOf(snapshot)) {
676
+ if (node === null || typeof node !== "object") continue;
677
+ const n = node;
678
+ if (n.kind !== "tool-call") continue;
679
+ const root = n.data?.root;
680
+ if (root === null || typeof root !== "object") continue;
681
+ const r = root;
682
+ const callId = typeof r.callId === "string" ? r.callId : "";
683
+ if (callId === "") continue;
684
+ if ((typeof r.name === "string" ? r.name : typeof r.call?.name === "string" ? r.call.name : "") !== "ask_user_question") continue;
685
+ const asked = questionsTextOf(typeof r.argsRaw === "string" ? r.argsRaw : typeof r.call?.argsRaw === "string" ? r.call.argsRaw : "");
686
+ if (asked === "") continue;
687
+ out.push({
688
+ seq: typeof n.anchorSeq === "number" ? n.anchorSeq : 0,
689
+ time: typeof r.time === "number" ? r.time : 0,
690
+ text: asked,
691
+ hasImage: false,
692
+ key: typeof n.key === "string" ? n.key : void 0,
693
+ anchor: `call:${callId}`,
694
+ qa: true
695
+ });
696
+ }
697
+ out.sort((a, b) => a.seq - b.seq);
698
+ return out;
699
+ }
700
+ /** 问答条目的预览文本:取 `arguments` JSON 里的问题标题(多问用 ` / ` 连)。 */
701
+ function questionsTextOf(argsRaw) {
702
+ if (typeof argsRaw !== "string" || argsRaw.trim() === "") return "";
703
+ let parsed;
704
+ try {
705
+ parsed = JSON.parse(argsRaw);
706
+ } catch {
707
+ return "";
708
+ }
709
+ const list = parsed?.questions;
710
+ if (!Array.isArray(list)) return "";
711
+ const parts = [];
712
+ for (const q of list) {
713
+ if (q === null || typeof q !== "object") continue;
714
+ const o = q;
715
+ const text = typeof o.question === "string" && o.question !== "" ? o.question : typeof o.header === "string" ? o.header : "";
716
+ if (text !== "") parts.push(text);
717
+ }
718
+ return parts.join(" / ").trim().slice(0, 80);
719
+ }
720
+ /** Resolve the DOM anchor key (`data-chat-anchor-key`) for one rail message. */
616
721
  function anchorKeyOf(m) {
722
+ if (typeof m.anchor === "string" && m.anchor !== "") return m.anchor;
617
723
  if (typeof m.key === "string" && m.key !== "") return m.key;
618
724
  if (typeof m.id === "string" && m.id !== "") return "13:input-message" + m.id;
619
725
  }
726
+ /** Resolve the **chat node key** for one rail message (used for load checks). */
727
+ function nodeKeyOf(m) {
728
+ return typeof m.key === "string" && m.key !== "" ? m.key : anchorKeyOf(m);
729
+ }
620
730
  /** Full text of a rail message from the loaded chat nodes (uncapped),
621
731
  * falling back to the projection preview when the node is not mounted. */
622
732
  function fullTextOf(m, nodeSnapshot) {
@@ -663,37 +773,42 @@ window.__ModuleLoader__.load({
663
773
  * DSH, where the Session snapshot no longer carries chat.nodes — falling back
664
774
  * to the legacy Session snapshot's `chat.nodes` Map without it.
665
775
  */
666
- async function jumpToMessage(sessionsService, sessionId, key, nodeOf, onProgress, signal, targetSeq) {
776
+ async function jumpToMessage(sessionsService, sessionId, key, nodeOf, onProgress, signal, targetSeq, onJump) {
667
777
  const session = sessionsService.binding(sessionId)?.session;
668
778
  if (session === void 0) return false;
669
779
  const isLoaded = nodeOf === void 0 ? (k) => session.getSnapshot().chat?.nodes?.get(k) !== void 0 : nodeOf;
670
780
  let pages = 0;
671
781
  let loaded = false;
672
- const jumpLoadThrough = session.loadThrough;
673
- if (typeof jumpLoadThrough === "function" && targetSeq !== void 0 && Number.isSafeInteger(targetSeq)) {
674
- let spin = 0;
675
- while (spin++ < 120) {
676
- if (signal?.aborted) return false;
677
- if (session.getSnapshot()?.loadingOlder !== true) break;
678
- await delay(50);
679
- }
680
- if (signal?.aborted) return false;
681
- await jumpLoadThrough(targetSeq);
682
- let spinNode = 0;
683
- while (spinNode++ < 100) {
782
+ const jumpSession = session;
783
+ if (typeof jumpSession.loadThrough === "function" && targetSeq !== void 0 && Number.isSafeInteger(targetSeq)) {
784
+ if (!isLoaded(key)) {
785
+ let spin = 0;
786
+ while (spin++ < 120) {
787
+ if (signal?.aborted) return false;
788
+ if (session.getSnapshot()?.loadingOlder !== true) break;
789
+ await delay(50);
790
+ }
684
791
  if (signal?.aborted) return false;
685
- if (isLoaded(key)) {
686
- loaded = true;
687
- break;
792
+ onJump?.("paging");
793
+ await jumpSession.loadThrough(targetSeq);
794
+ let spinNode = 0;
795
+ while (spinNode++ < 100) {
796
+ if (signal?.aborted) return false;
797
+ if (isLoaded(key)) {
798
+ loaded = true;
799
+ break;
800
+ }
801
+ await delay(50);
802
+ }
803
+ if (!loaded) {
804
+ console.warn(`[chat-rail] jumpToMessage: node "${key}" not loaded after loadThrough(${String(targetSeq)})`);
805
+ return false;
688
806
  }
689
- await delay(50);
690
- }
691
- if (!loaded) {
692
- console.warn(`[chat-rail] jumpToMessage: node "${key}" not loaded after loadThrough(${String(targetSeq)})`);
693
- return false;
694
807
  }
808
+ loaded = true;
695
809
  } else {
696
810
  let guard = 0;
811
+ let reportedPaging = false;
697
812
  while (guard++ < 120) {
698
813
  if (signal?.aborted) return false;
699
814
  const snapshot = session.getSnapshot();
@@ -706,6 +821,10 @@ window.__ModuleLoader__.load({
706
821
  await delay(50);
707
822
  continue;
708
823
  }
824
+ if (!reportedPaging) {
825
+ onJump?.("paging");
826
+ reportedPaging = true;
827
+ }
709
828
  await session.loadOlder();
710
829
  pages++;
711
830
  onProgress?.(pages);
@@ -726,15 +845,15 @@ window.__ModuleLoader__.load({
726
845
  await delay(50);
727
846
  }
728
847
  if (row === null) return false;
848
+ onJump?.("landed");
729
849
  const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
730
- const scrollToRow = () => {
850
+ const scrollToRow = (instant = false) => {
731
851
  const viewRect = scrollport.getBoundingClientRect();
732
852
  const rowRect = row.getBoundingClientRect();
733
- const target = scrollport.scrollTop + (rowRect.top - viewRect.top) - (viewRect.height - rowRect.height) / 2;
734
- if (target < 0) return;
853
+ const target = Math.max(0, scrollport.scrollTop + (rowRect.top - viewRect.top) - (viewRect.height - rowRect.height) / 2);
735
854
  scrollport.scrollTo({
736
855
  top: target,
737
- behavior: reducedMotion ? "auto" : "smooth"
856
+ behavior: instant || reducedMotion ? "auto" : "smooth"
738
857
  });
739
858
  };
740
859
  const rowDelta = () => {
@@ -765,7 +884,7 @@ window.__ModuleLoader__.load({
765
884
  await delay(600);
766
885
  if (signal?.aborted) return false;
767
886
  if (rowDelta() <= Math.max(120, scrollport.getBoundingClientRect().height * .2)) break;
768
- scrollToRow();
887
+ scrollToRow(true);
769
888
  }
770
889
  return true;
771
890
  }
@@ -855,6 +974,7 @@ window.__ModuleLoader__.load({
855
974
  let messages = [];
856
975
  if (Array.isArray(projected?.messages) && projected.messages.length > 0) messages = projected.messages.map(normalize).filter((m) => m !== null);
857
976
  if (messages.length === 0) messages = collectFromNodes(nodeSnapshot);
977
+ const qaMessages = collectQaFromNodes(nodeSnapshot);
858
978
  const showOfficial = (0, react.useSyncExternalStore)(railSettingsScope !== null ? (cb) => railSettingsScope.subscribe(cb) : NOOP_SUB, () => railSettingsScope?.getSnapshot() ?? { value: void 0 }).value?.showOfficialNavigator === true;
859
979
  const applyOfficialMode = (on) => {
860
980
  syncOfficialHide(on);
@@ -866,6 +986,39 @@ window.__ModuleLoader__.load({
866
986
  (0, react.useEffect)(() => {
867
987
  applyOfficialMode(showOfficial);
868
988
  }, [showOfficial]);
989
+ const [anchor, setAnchor] = (0, react.useState)(null);
990
+ (0, react.useEffect)(() => {
991
+ const measure = () => {
992
+ if (typeof document === "undefined") return;
993
+ const scrollport = document.querySelector("[data-conversation-scroll]");
994
+ if (scrollport === null) {
995
+ setAnchor(null);
996
+ return;
997
+ }
998
+ const rect = scrollport.getBoundingClientRect();
999
+ if (rect.width === 0) {
1000
+ setAnchor(null);
1001
+ return;
1002
+ }
1003
+ const next = {
1004
+ right: Math.max(0, Math.round(window.innerWidth - rect.right)),
1005
+ width: Math.round(rect.width)
1006
+ };
1007
+ setAnchor((prev) => prev !== null && prev.right === next.right && prev.width === next.width ? prev : next);
1008
+ };
1009
+ measure();
1010
+ const scrollport = document.querySelector("[data-conversation-scroll]");
1011
+ const observer = typeof ResizeObserver === "undefined" || scrollport === null ? null : new ResizeObserver(measure);
1012
+ if (observer !== null && scrollport !== null) observer.observe(scrollport);
1013
+ window.addEventListener("resize", measure);
1014
+ const timer = window.setInterval(measure, 400);
1015
+ return () => {
1016
+ if (observer !== null) observer.disconnect();
1017
+ window.removeEventListener("resize", measure);
1018
+ window.clearInterval(timer);
1019
+ };
1020
+ }, [sessionId]);
1021
+ const spaceTight = anchor !== null && anchor.width < RAIL_MIN_SPACE;
869
1022
  const favorites = (0, react.useSyncExternalStore)(subscribeFavorites, favoritesSnapshot);
870
1023
  const [favOnly, setFavOnly] = (0, react.useState)(false);
871
1024
  const showFavToggle = (sessionId === void 0 ? 0 : messages.filter((m) => isFavorite(favorites, sessionId, favoriteIdOfMessage(m))).length) > 0;
@@ -878,6 +1031,7 @@ window.__ModuleLoader__.load({
878
1031
  const [jumping, setJumping] = (0, react.useState)(false);
879
1032
  const [tip, setTip] = (0, react.useState)(null);
880
1033
  const navRef = (0, react.useRef)(null);
1034
+ const listRef = (0, react.useRef)(null);
881
1035
  const expandedRef = (0, react.useRef)(false);
882
1036
  const lastPointerRef = (0, react.useRef)(null);
883
1037
  const jumpAbortRef = (0, react.useRef)(null);
@@ -916,9 +1070,21 @@ window.__ModuleLoader__.load({
916
1070
  }
917
1071
  positionTip(index);
918
1072
  };
1073
+ const tipCloseTimerRef = (0, react.useRef)(null);
1074
+ const cancelTipClose = () => {
1075
+ if (tipCloseTimerRef.current !== null) {
1076
+ clearTimeout(tipCloseTimerRef.current);
1077
+ tipCloseTimerRef.current = null;
1078
+ }
1079
+ };
919
1080
  const handleItemLeave = (index) => {
920
- setTip((prev) => prev?.index === index ? null : prev);
1081
+ cancelTipClose();
1082
+ tipCloseTimerRef.current = setTimeout(() => {
1083
+ tipCloseTimerRef.current = null;
1084
+ setTip((prev) => prev?.index === index ? null : prev);
1085
+ }, 180);
921
1086
  };
1087
+ (0, react.useEffect)(() => cancelTipClose, []);
922
1088
  (0, react.useEffect)(() => {
923
1089
  if (!show) {
924
1090
  expandedRef.current = false;
@@ -1048,7 +1214,7 @@ window.__ModuleLoader__.load({
1048
1214
  const rect = sp.getBoundingClientRect();
1049
1215
  if (rect.height === 0) return;
1050
1216
  const line = rect.top + rect.height * .4;
1051
- const rows = sp.querySelectorAll("[data-chat-anchor-key^=\"13:input-message\"]");
1217
+ const rows = sp.querySelectorAll("[data-chat-anchor-key^=\"13:input-message\"],[data-chat-anchor-key^=\"call:\"]");
1052
1218
  let best = -1;
1053
1219
  let bestDist = Infinity;
1054
1220
  for (const row of rows) {
@@ -1085,7 +1251,7 @@ window.__ModuleLoader__.load({
1085
1251
  }, [sessionId, effectiveMessages.length]);
1086
1252
  (0, react.useEffect)(() => {
1087
1253
  if (activeIndex < 0) return;
1088
- const el = navRef.current;
1254
+ const el = listRef.current;
1089
1255
  if (el === null) return;
1090
1256
  const item = el.querySelector(`[data-crl-index="${activeIndex}"]`);
1091
1257
  if (item === null) return;
@@ -1104,6 +1270,8 @@ window.__ModuleLoader__.load({
1104
1270
  type: "button",
1105
1271
  key: m.seq,
1106
1272
  "data-crl-index": String(i),
1273
+ ...key !== void 0 ? { "data-crl-anchor": key } : {},
1274
+ ...m.qa === true ? { "data-crl-qa": "true" } : {},
1107
1275
  className: S.item + (activeIndex === i ? ` ${S.itemActive}` : "") + (starred ? ` ${S.favItem}` : ""),
1108
1276
  "aria-label": `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,
1109
1277
  "aria-current": activeIndex === i ? "location" : void 0,
@@ -1113,8 +1281,7 @@ window.__ModuleLoader__.load({
1113
1281
  jumpAbortRef.current?.abort();
1114
1282
  const controller = new AbortController();
1115
1283
  jumpAbortRef.current = controller;
1116
- setJumping(true);
1117
- jumpToMessage(sessionsService, sessionId, key, (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, m.seq).finally(() => setJumping(false));
1284
+ jumpToMessage(sessionsService, sessionId, nodeKeyOf(m), (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, m.seq, (phase) => setJumping(phase === "paging")).finally(() => setJumping(false));
1118
1285
  },
1119
1286
  onMouseEnter: () => handleItemEnter(i),
1120
1287
  onMouseLeave: () => handleItemLeave(i),
@@ -1174,7 +1341,8 @@ window.__ModuleLoader__.load({
1174
1341
  });
1175
1342
  return (0, react_dom.createPortal)([(0, react.createElement)("div", {
1176
1343
  ref: navRef,
1177
- className: S.nav + (show ? ` ${S.navShow}` : ""),
1344
+ className: S.nav + (show ? ` ${S.navShow}` : "") + (spaceTight ? ` ${NAV_HIDDEN_CLASS}` : ""),
1345
+ ...anchor !== null ? { style: { right: `${anchor.right + 3}px` } } : {},
1178
1346
  role: "navigation",
1179
1347
  "aria-label": t.railLabel,
1180
1348
  onMouseEnter: () => setShow(true),
@@ -1204,7 +1372,11 @@ window.__ModuleLoader__.load({
1204
1372
  className: S.loading,
1205
1373
  key: "loading"
1206
1374
  }, (0, react.createElement)("span", { className: S.loadingLabel }, t.loading)) : null,
1207
- ...items
1375
+ (0, react.createElement)("div", {
1376
+ ref: listRef,
1377
+ key: "list",
1378
+ className: S.list
1379
+ }, ...items)
1208
1380
  ]
1209
1381
  }), tip !== null && tipIndex >= 0 && tipIndex < messages.length ? (0, react.createElement)("div", {
1210
1382
  className: S.tip,
@@ -1212,7 +1384,9 @@ window.__ModuleLoader__.load({
1212
1384
  left: `${tip.x}px`,
1213
1385
  top: `${tip.y}px`,
1214
1386
  transform: "translateX(-100%)"
1215
- }
1387
+ },
1388
+ onMouseEnter: () => cancelTipClose(),
1389
+ onMouseLeave: () => setTip(null)
1216
1390
  }, (() => {
1217
1391
  const m = messages[tipIndex];
1218
1392
  const specs = tipImagesOf(m, nodeSnapshot);
@@ -1236,6 +1410,52 @@ window.__ModuleLoader__.load({
1236
1410
  key: "badge"
1237
1411
  }, t.hasImage));
1238
1412
  children.push((0, react.createElement)("span", { key: "text" }, fullTextOf(m, nodeSnapshot) || t.noText));
1413
+ const qaForTip = [];
1414
+ for (let qi = qaMessages.length - 1; qi >= 0; qi--) if (qaMessages[qi].seq >= m.seq) qaForTip.unshift(qaMessages[qi]);
1415
+ if (qaForTip.length > 0) children.push((0, react.createElement)("div", {
1416
+ className: S.tipQaWrap,
1417
+ key: "qa"
1418
+ }, qaForTip.map((q) => (0, react.createElement)("button", {
1419
+ type: "button",
1420
+ key: q.key ?? q.anchor,
1421
+ className: S.tipQa,
1422
+ title: q.text,
1423
+ onClick: (e) => {
1424
+ e.stopPropagation();
1425
+ if (jumping) return;
1426
+ jumpAbortRef.current?.abort();
1427
+ const controller = new AbortController();
1428
+ jumpAbortRef.current = controller;
1429
+ jumpToMessage(sessionsService, sessionId, nodeKeyOf(q), (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, q.seq, (phase) => setJumping(phase === "paging")).finally(() => setJumping(false));
1430
+ },
1431
+ children: [(0, react.createElement)("svg", {
1432
+ key: "icon",
1433
+ className: S.tipQaIcon,
1434
+ viewBox: "0 0 16 16",
1435
+ width: 13,
1436
+ height: 13,
1437
+ fill: "none",
1438
+ stroke: "currentColor",
1439
+ strokeWidth: 1.6,
1440
+ strokeLinecap: "round",
1441
+ strokeLinejoin: "round",
1442
+ "aria-hidden": true
1443
+ }, (0, react.createElement)("circle", {
1444
+ cx: 8,
1445
+ cy: 8,
1446
+ r: 6.5,
1447
+ "aria-hidden": true
1448
+ }), (0, react.createElement)("path", {
1449
+ d: "M6.06 6a2 2 0 0 1 3.89.67c0 1.33-2 2-2 2",
1450
+ "aria-hidden": true
1451
+ }), (0, react.createElement)("path", {
1452
+ d: "M8 11.33h.01",
1453
+ "aria-hidden": true
1454
+ })), (0, react.createElement)("span", {
1455
+ className: S.tipQaText,
1456
+ key: "txt"
1457
+ }, q.text)]
1458
+ }))));
1239
1459
  return children;
1240
1460
  })()) : null], document.body);
1241
1461
  }
@@ -1312,6 +1532,7 @@ window.__ModuleLoader__.load({
1312
1532
  exports.chatNodeOf = chatNodeOf;
1313
1533
  exports.clearRowActions = clearRowActions;
1314
1534
  exports.collectFromNodes = collectFromNodes;
1535
+ exports.collectQaFromNodes = collectQaFromNodes;
1315
1536
  exports.favoriteIdsOf = favoriteIdsOf;
1316
1537
  exports.imageSpecsOfContent = imageSpecsOfContent;
1317
1538
  exports.inject = inject;