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

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
@@ -94,12 +94,11 @@ window.__ModuleLoader__.load({
94
94
  return `${d.getMonth() + 1}/${d.getDate()}`;
95
95
  }
96
96
  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}",
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:none;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)}",
99
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)}",
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{width:0;height:0}",
103
102
  ".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
103
  ".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
104
  ".crl_loading .crl_loadingLabel{display:none}",
@@ -663,37 +662,42 @@ window.__ModuleLoader__.load({
663
662
  * DSH, where the Session snapshot no longer carries chat.nodes — falling back
664
663
  * to the legacy Session snapshot's `chat.nodes` Map without it.
665
664
  */
666
- async function jumpToMessage(sessionsService, sessionId, key, nodeOf, onProgress, signal, targetSeq) {
665
+ async function jumpToMessage(sessionsService, sessionId, key, nodeOf, onProgress, signal, targetSeq, onJump) {
667
666
  const session = sessionsService.binding(sessionId)?.session;
668
667
  if (session === void 0) return false;
669
668
  const isLoaded = nodeOf === void 0 ? (k) => session.getSnapshot().chat?.nodes?.get(k) !== void 0 : nodeOf;
670
669
  let pages = 0;
671
670
  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) {
671
+ const jumpSession = session;
672
+ if (typeof jumpSession.loadThrough === "function" && targetSeq !== void 0 && Number.isSafeInteger(targetSeq)) {
673
+ if (!isLoaded(key)) {
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
+ }
684
680
  if (signal?.aborted) return false;
685
- if (isLoaded(key)) {
686
- loaded = true;
687
- break;
681
+ onJump?.("paging");
682
+ await jumpSession.loadThrough(targetSeq);
683
+ let spinNode = 0;
684
+ while (spinNode++ < 100) {
685
+ if (signal?.aborted) return false;
686
+ if (isLoaded(key)) {
687
+ loaded = true;
688
+ break;
689
+ }
690
+ await delay(50);
691
+ }
692
+ if (!loaded) {
693
+ console.warn(`[chat-rail] jumpToMessage: node "${key}" not loaded after loadThrough(${String(targetSeq)})`);
694
+ return false;
688
695
  }
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
696
  }
697
+ loaded = true;
695
698
  } else {
696
699
  let guard = 0;
700
+ let reportedPaging = false;
697
701
  while (guard++ < 120) {
698
702
  if (signal?.aborted) return false;
699
703
  const snapshot = session.getSnapshot();
@@ -706,6 +710,10 @@ window.__ModuleLoader__.load({
706
710
  await delay(50);
707
711
  continue;
708
712
  }
713
+ if (!reportedPaging) {
714
+ onJump?.("paging");
715
+ reportedPaging = true;
716
+ }
709
717
  await session.loadOlder();
710
718
  pages++;
711
719
  onProgress?.(pages);
@@ -726,12 +734,12 @@ window.__ModuleLoader__.load({
726
734
  await delay(50);
727
735
  }
728
736
  if (row === null) return false;
737
+ onJump?.("landed");
729
738
  const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
730
739
  const scrollToRow = () => {
731
740
  const viewRect = scrollport.getBoundingClientRect();
732
741
  const rowRect = row.getBoundingClientRect();
733
- const target = scrollport.scrollTop + (rowRect.top - viewRect.top) - (viewRect.height - rowRect.height) / 2;
734
- if (target < 0) return;
742
+ const target = Math.max(0, scrollport.scrollTop + (rowRect.top - viewRect.top) - (viewRect.height - rowRect.height) / 2);
735
743
  scrollport.scrollTo({
736
744
  top: target,
737
745
  behavior: reducedMotion ? "auto" : "smooth"
@@ -1113,8 +1121,7 @@ window.__ModuleLoader__.load({
1113
1121
  jumpAbortRef.current?.abort();
1114
1122
  const controller = new AbortController();
1115
1123
  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));
1124
+ jumpToMessage(sessionsService, sessionId, key, (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, m.seq, (phase) => setJumping(phase === "paging")).finally(() => setJumping(false));
1118
1125
  },
1119
1126
  onMouseEnter: () => handleItemEnter(i),
1120
1127
  onMouseLeave: () => handleItemLeave(i),
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useState","createElement","useSyncExternalStore","useRef","useMemo","useCallback","createPortal"],"sources":["../src/client/index.tsx"],"sourcesContent":["/**\r\n * @max-null/dsh-chat-rail — web client half.\r\n *\r\n * Right-edge conversation navigation rail: one indicator per user message,\r\n * scroll-spy highlight of the reading position, hover preview, click to jump.\r\n *\r\n * Two deliberate fixes over the dsh-chat-timeline reference:\r\n *\r\n * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout\r\n * push through the `--dsh-sidebar-width` CSS variable on `:root` (the\r\n * `#root` margin-right transition reads the same variable). The rail's\r\n * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same\r\n * transition timing, so rail and scrollport move together while the panel\r\n * expands — no \"panel first, scrollbar later\" lag.\r\n *\r\n * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)\r\n * lines — nearly invisible on a light background. Here the rail keeps a\r\n * constant translucent capsule, thicker/darker indicator lines, and an\r\n * always-visible collapsed state (no \"reveal on pointer proximity\").\r\n *\r\n * Data: host `chatRail` projection first, loaded chat nodes fallback, then a\r\n * background loadOlder loop (stopped as soon as the projection delivers).\r\n * Mounted in conversation.input.dock, portal-rendered to body.\r\n */\r\n\r\nimport { createElement, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\r\nimport type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'\r\n// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry).\r\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\r\n\r\nexport const inject = ['slots', 'sessions', 'conversation', 'settingsScope']\r\n\r\n// ---- i18n (DSH locale-aware, zh/en) ----\r\ntype LocaleId = 'zh' | 'en'\r\nconst STRINGS: Record<LocaleId, Record<string, string>> = {\r\n zh: {\r\n railLabel: '消息导航',\r\n roleUser: '用户',\r\n noText: '(无文本内容)',\r\n ariaJump: '跳转到消息',\r\n hasImage: '含图片',\r\n loading: '加载中…',\r\n timeJustNow: '刚刚',\r\n timeMinutes: '{n}分钟前',\r\n timeHours: '{n}小时前',\r\n timeDays: '{n}天前',\r\n fav: '收藏消息',\r\n unfav: '取消收藏',\r\n fill: '填充到输入框',\r\n filled: '已填入输入框',\r\n favOnly: '只显示收藏',\r\n settings: '设置',\r\n cardName: '消息导航条',\r\n cardDesc: '画卷式消息导航栏(chat-rail)——每条用户消息一个指示点,scroll-spy 跟随',\r\n officialNav: '使用官方轮次导航条(对比模式)',\r\n officialNavHint: '开启后隐藏本导航条、显示官方 TurnNavigator——便于与官方轮次导航对比(免重启即时生效)',\r\n },\r\n en: {\r\n railLabel: 'Message rail',\r\n roleUser: 'User',\r\n noText: '(no text)',\r\n ariaJump: 'Jump to message',\r\n hasImage: 'Has image',\r\n loading: 'Loading…',\r\n timeJustNow: 'just now',\r\n timeMinutes: '{n}m ago',\r\n timeHours: '{n}h ago',\r\n timeDays: '{n}d ago',\r\n fav: 'Bookmark message',\r\n unfav: 'Remove bookmark',\r\n fill: 'Fill into input',\r\n filled: 'Filled into input',\r\n favOnly: 'Bookmarks only',\r\n settings: 'Settings',\r\n cardName: 'Message rail',\r\n cardDesc: 'Canvas-style message navigation (chat-rail) — one dot per user message, scroll-spy follow',\r\n officialNav: 'Use the official turn navigation (comparison mode)',\r\n officialNavHint: 'Hides this rail and shows the official TurnNavigator — for A/B comparison with the official turn rail (applies instantly, no restart).',\r\n },\r\n}\r\n\r\n/** Compact relative time for a message timestamp (zh/en via template). */\r\nfunction relativeTime(ts: number, s: Record<string, string>): string {\r\n const diff = Date.now() - ts\r\n if (diff < 60_000) return s.timeJustNow\r\n if (diff < 3_600_000) return s.timeMinutes.replace('{n}', String(Math.floor(diff / 60_000)))\r\n if (diff < 86_400_000) return s.timeHours.replace('{n}', String(Math.floor(diff / 3_600_000)))\r\n if (diff < 7 * 86_400_000) return s.timeDays.replace('{n}', String(Math.floor(diff / 86_400_000)))\r\n const d = new Date(ts)\r\n return `${d.getMonth() + 1}/${d.getDate()}`\r\n}\r\n\r\n// ---- styles ----\r\n// 画卷式导航:单个容器,折叠态 36px 竖条;hover 时宽度从右往左展开到 280px\r\n// (打开画卷效果)。容器垂直方向在「去掉底栏后的可用区域」内居中:\r\n// top = (100vh - 底栏高)/2 + translateY(-50%),底栏收起时正中、展开时上移避让。\r\n// right 跟随 --dsh-sidebar-width、top 跟随 --dsh-sidebar-height,与 better-sidebar\r\n// 面板共享同一 CSS 变量 + transition,动画同步。\r\nconst css = [\r\n '.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}',\r\n '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}',\r\n '.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)}',\r\n '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)}',\r\n '.crl_nav::-webkit-scrollbar{width:4px}',\r\n '.crl_nav::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);border-radius:4px}',\r\n // Jump-in-progress indicator: sticky row pinned at the rail top. The spinner\r\n // icon is always visible; the \"Loading…\" label only appears once expanded\r\n // (in the collapsed 36px rail the text would overflow the capsule).\r\n '.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}',\r\n '.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}',\r\n '.crl_loading .crl_loadingLabel{display:none}',\r\n '.crl_show .crl_loading .crl_loadingLabel{display:inline}',\r\n '@keyframes crl-spin{to{transform:rotate(360deg)}}',\r\n // Item row: always flex-end (the indicator rides the right edge through the\r\n // width animation, never jumping). The collapsed padding (0 9px) centers the\r\n // 18px indicator in the 36px capsule; the expanded padding (0 14px) clears\r\n // the text. Padding transitions with the same timing as the width, so the\r\n // indicator glides smoothly instead of snapping when text hides/shows.\r\n '.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}',\r\n '.crl_show .crl_item{padding:0 14px}',\r\n '.crl_item:hover{color:rgba(0,0,0,.95)}',\r\n '.crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_item,[data-theme=\\'dark\\'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}',\r\n 'body[data-ds-dark-theme] .crl_item:hover,[data-theme=\\'dark\\'] .crl_item:hover,.dark .crl_item:hover{color:rgba(255,255,255,.95)}',\r\n // Title / ordinal / time: display:none in collapsed state (so they take no\r\n // flex space and the indicator line stays centered), revealed on expand.\r\n // The ordinal has a fixed width (right-aligned) so message text starts at\r\n // the same x across items; time sits AFTER the title (no fixed width, so it\r\n // never shrinks the text area).\r\n '.crl_title{display:none;font-size:13px;line-height:20px;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;flex:1;min-width:0;text-align:left;overflow:hidden;color:inherit}',\r\n '.crl_show .crl_title{display:block;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_active .crl_title{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:500}',\r\n '.crl_num{display:none;flex-shrink:0;width:28px;font-size:10px;line-height:20px;color:rgba(0,0,0,.35);margin-right:8px;text-align:right;user-select:none}',\r\n '.crl_show .crl_num{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_num,[data-theme=\\'dark\\'] .crl_num,.dark .crl_num{color:rgba(255,255,255,.35)}',\r\n '.crl_time{display:none;flex-shrink:0;font-size:10px;line-height:20px;color:rgba(0,0,0,.28);margin-right:10px;user-select:none;white-space:nowrap}',\r\n '.crl_show .crl_time{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_time,[data-theme=\\'dark\\'] .crl_time,.dark .crl_time{color:rgba(255,255,255,.28)}',\r\n '@keyframes crl-fade{from{opacity:0}to{opacity:1}}',\r\n // Indicator line: 10×3px, clearly visible in both themes.\r\n '.crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}',\r\n '.crl_show .crl_ind{margin-left:8px}',\r\n // Image badge: hidden in the collapsed state (the rail is a clean 36px\r\n // capsule of indicator lines; an icon there would crowd and distract) and\r\n // revealed with the other labels on expand. A quiet stroke glyph in a muted\r\n // tone that ignores the active-blue text color.\r\n '.crl_img{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-left:6px;color:rgba(0,0,0,.38)}',\r\n '.crl_show .crl_img{display:inline-flex;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_img,[data-theme=\\'dark\\'] .crl_img,.dark .crl_img{color:rgba(255,255,255,.38)}',\r\n '.crl_line{background-color:rgba(0,0,0,.45);border-radius:4px;flex-shrink:0;width:10px;height:3px;transition:background-color .2s ease,transform .2s ease}',\r\n '.crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}',\r\n '.crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe);transform-origin:50%;transform:scale(1.4);box-shadow:0 0 6px var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_line,[data-theme=\\'dark\\'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}',\r\n '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)}',\r\n '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)}',\r\n // Full-content hover panel: floats to the LEFT of the expanded rail list.\r\n '.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}',\r\n '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)))}',\r\n // Tip thumbnail gallery: stacked above the text, one row per image, capped\r\n // height so a tall screenshot cannot swallow the preview panel. The\r\n // placeholder pulses while session.readAttachment resolves the bytes; a\r\n // failed load removes the row entirely rather than showing a broken image.\r\n '.crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}',\r\n '.crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}',\r\n '.crl_tipImg{display:block;max-width:100%;max-height:150px;width:auto;height:auto;object-fit:contain;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)))}',\r\n '.crl_tipImgCount{position:absolute;right:6px;bottom:6px;padding:0 6px;font-size:10px;line-height:16px;border-radius:9px;background:rgba(0,0,0,.55);color:rgba(255,255,255,.95);pointer-events:none}',\r\n '.crl_tipImgPh{flex-shrink:0;height:56px;border-radius:8px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.05)));animation:crl-pulse 1.2s ease-in-out infinite}',\r\n 'body[data-ds-dark-theme] .crl_tipImg,[data-theme=\\'dark\\'] .crl_tipImg,.dark .crl_tipImg{border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}',\r\n '.crl_tipBadge{display:inline-block;margin-bottom:8px;padding:0 7px;font-size:10px;line-height:16px;border-radius:9px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.06)));color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.45)))}',\r\n '@keyframes crl-pulse{0%,100%{opacity:.45}50%{opacity:.85}}',\r\n // Message action buttons injected next to the copy button (DOM row: the\r\n // user-message IconActions row). The host rows use 28px round icon-action\r\n // buttons (padding 6, 16px glyphs, DSW token colors, hover fill) — the\r\n // injected buttons mirror the exact geometry and token surface so the star\r\n // and plus read as native siblings of copy/branch. The star lights\r\n // yellowish (#ffd166, milestone-compatible) once favorited.\r\n // Tooltip: same dark pill as the host Tooltip (black rounded label, no\r\n // native title balloon). The delay mirrors the host tooltip's settle; the\r\n // fade is short so the pill appears right beside the hovered button.\r\n '.crl_msgAct{position:relative;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:0;padding:6px;border:none;border-radius:28px;background:transparent;color:var(--dsw-alias-label-tertiary,rgba(0,0,0,.42));cursor:pointer;transition:background .15s ease,color .15s ease}',\r\n '.crl_msgAct:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06));color:var(--dsw-alias-label-secondary,rgba(0,0,0,.72))}',\r\n '.crl_msgAct:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4d6bfe);outline-offset:2px}',\r\n '.crl_msgAct[data-tip]:hover::after{content:attr(data-tip);position:absolute;left:50%;bottom:calc(100% + 8px);transform:translateX(-50%);padding:5px 10px;border-radius:8px;background:rgba(24,28,36,.94);color:rgba(255,255,255,.95);font-size:12px;line-height:1.4;white-space:nowrap;pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}',\r\n '.crl_msgAct[data-tip]:hover::before{content:\"\";position:absolute;left:50%;bottom:calc(100% + 3px);transform:translateX(-50%);border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid rgba(24,28,36,.94);pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}',\r\n '@keyframes crl-tip-in{to{opacity:1}}',\r\n '.crl_msgAct.crl_fav.crl_on{color:#ffd166}',\r\n '.crl_msgAct.crl_fav.crl_on svg{fill:currentColor}',\r\n '.crl_msgAct.crl_fav.crl_on:hover{color:#ffd166}',\r\n // Rail row favorite badge + yellow indicator line for favorited messages.\r\n '.crl_favStar{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-right:6px;color:#ffd166}',\r\n '.crl_show .crl_favStar{display:inline-flex;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_favItem .crl_line{background-color:#ffd166}',\r\n '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}',\r\n // A favorited line that is also the current reading position keeps its\r\n // yellow identity — the generic active glow is blue, which pairs miserably\r\n // with the gold line, so the favorite-active state recasts the glow in the\r\n // same warm tone.\r\n '.crl_item.crl_favItem.crl_active .crl_line{background-color:#ffd166;box-shadow:0 0 6px #ffd166}',\r\n // Rail-top favorites-only toggle: a round icon pill in the collapsed state;\r\n // on expand it becomes a full-width label row (star + \"bookmarks only\") so\r\n // the control reads as a real rail header action instead of a stray dot.\r\n // STICKY: the expanded rail's rows scroll inside the capsule (overflow-y),\r\n // and the pill must stay pinned at the top instead of scrolling away with\r\n // them. The capsule has padding:10px 0 — sticky `top:0` would leave the\r\n // last 10px of the scrollport uncovered, letting a passing row peek through\r\n // above the pill. top:-10px shifts the pin edge to the capsule's true top\r\n // (padding included), so rows scroll fully underneath.\r\n '.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}',\r\n '.crl_favToggle:hover{background:rgba(0,0,0,.07);color:rgba(0,0,0,.75)}',\r\n '.crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.14)}',\r\n '.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}',\r\n '.crl_favToggleLabel{display:none}',\r\n '.crl_show .crl_favToggleLabel{display:inline-block;min-width:0;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}',\r\n 'body[data-ds-dark-theme] .crl_favToggle,[data-theme=\\'dark\\'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4)}',\r\n '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)}',\r\n '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)}',\r\n '@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}',\r\n '.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}',\r\n '.crlSetCard:hover{border-color:var(--dsw-alias-label-dimmed)}',\r\n '.crlSetCardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}',\r\n '.crlSetHeader{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px}',\r\n '.crlSetHeadText{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}',\r\n '.crlSetName{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}',\r\n '.crlSetDesc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}',\r\n '.crlSetChevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s}',\r\n '.crlSetChevronOn{transform:rotate(180deg)}',\r\n '.crlSetBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}',\r\n '.crlSetRow{position:relative;display:flex;align-items:center;gap:10px;padding:10px 0 4px;cursor:default}',\r\n '.crlSetRowLabel{flex:1;min-width:0;font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}',\r\n '.crlSetHint{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary);font-weight:400}',\r\n '.crlSetSwitch{flex:none;width:40px;height:22px;border:none;border-radius:11px;cursor:pointer;padding:0;background:var(--dsw-alias-border-l4,rgba(0,0,0,.16));transition:background .15s}',\r\n '.crlSetSwitch.on{background:var(--dsw-alias-state-business-primary,#4FC3F7)}',\r\n '.crlSetKnob{display:block;width:16px;height:16px;border-radius:8px;background:#fff;margin-left:2px;transition:margin-left .15s;pointer-events:none}',\r\n '.crlSetSwitch.on .crlSetKnob{margin-left:22px}',\r\n].join('')\r\n\r\n/** 屏蔽官方 TurnNavigator(轮次导航竖轨,DSH 0.1.2-alpha.1+)的独立样式(不再无条件注入——由\r\n * 「使用官方轮次导航条」设置开关控制):官方 rail 与本插件 rail 同处会话面板右缘且只支持\r\n * 「轮次」维度,默认隐藏官方、由本插件统一承担消息导航。锚点用 aria-label(zh/en 双文案)而非\r\n * CSS module 类名(如 hkplfa_slot,hash 随构建内容漂移)。 */\r\nconst HIDE_OFFICIAL_CSS = 'nav[aria-label=\"轮次导航\"],nav[aria-label=\"Turn navigation\"]{display:none !important}'\r\nconst HIDE_OFFICIAL_STYLE_ID = 'data-crl-hide-official'\r\n\r\n/** 按设置同步「屏蔽官方 TurnNavigator」样式(showOfficial=true=官方模式:不屏蔽)。 */\r\nexport function syncOfficialHide(showOfficial: boolean): void {\r\n const el = document.head.querySelector<HTMLStyleElement>(`style[${HIDE_OFFICIAL_STYLE_ID}]`)\r\n if (showOfficial) {\r\n if (el !== null) el.remove()\r\n return\r\n }\r\n if (el === null) {\r\n const tag = document.createElement('style')\r\n tag.setAttribute(HIDE_OFFICIAL_STYLE_ID, '')\r\n tag.textContent = HIDE_OFFICIAL_CSS\r\n document.head.appendChild(tag)\r\n }\r\n}\r\n\r\n/** 移除所有已注入的行内收藏/填充按钮(官方模式下的回溯清理)。 */\r\nexport function clearRowActions(): void {\r\n for (const row of document.querySelectorAll<HTMLElement>('[data-crl-actions=\"1\"]')) {\r\n for (const btn of row.querySelectorAll<HTMLElement>('[data-crl-star],[data-crl-fill]')) btn.remove()\r\n row.dataset.crlActions = ''\r\n }\r\n}\r\n\r\n// 「使用官方轮次导航条」设置(2026-09-05 对比模式):与 node-appearance 同构——官方\r\n// settingsScope(设置——插件页的「可配置插件」卡片,keyed by namespace 'chat-rail')。\r\ntype RailSettings = { showOfficialNavigator?: boolean }\r\nlet railSettingsScope: (({\r\n getSnapshot(): { value?: RailSettings }\r\n subscribe(listener: () => void): () => void\r\n}) & {\r\n set(key: 'showOfficialNavigator', value: boolean): Promise<unknown>\r\n}) | null = null\r\n\r\n/** client apply 时绑定(settingsScope 官方服务提供)。 */\r\nexport function bindRailSettingsScope(scope: unknown): void {\r\n if (typeof scope !== 'object' || scope === null) return\r\n railSettingsScope = (scope as {\r\n bind<T>(spec: { namespace: string }): {\r\n getSnapshot(): { value?: T }\r\n subscribe(listener: () => void): () => void\r\n set(key: keyof T & string, value: T[keyof T & string]): Promise<unknown>\r\n }\r\n }).bind<RailSettings>({ namespace: 'chat-rail' }) as never\r\n}\r\n\r\n/** No-op subscribe for useSyncExternalStore when the scope is not yet bound. */\r\nconst NOOP_SUB = (() => () => {}) as () => () => void\r\n\r\nconst STYLE_ID = '@max-null/dsh-chat-rail/styles.module.css'\r\nif (typeof document !== 'undefined' && document.querySelector(`style[data-plugin-css=\"${STYLE_ID}\"]`) === null) {\r\n const tag = document.createElement('style')\r\n tag.dataset.plugin = 'dsh-chat-rail'\r\n tag.dataset.pluginCss = STYLE_ID\r\n tag.textContent = css\r\n document.head.appendChild(tag)\r\n}\r\n\r\nconst S = {\r\n nav: 'crl_nav',\r\n navShow: 'crl_show',\r\n item: 'crl_item',\r\n itemActive: 'crl_active',\r\n title: 'crl_title',\r\n num: 'crl_num',\r\n time: 'crl_time',\r\n ind: 'crl_ind',\r\n line: 'crl_line',\r\n img: 'crl_img',\r\n loading: 'crl_loading',\r\n loadingLabel: 'crl_loadingLabel',\r\n tip: 'crl_tip',\r\n tipImgs: 'crl_tipImgs',\r\n tipImgWrap: 'crl_tipImgWrap',\r\n tipImg: 'crl_tipImg',\r\n tipImgCount: 'crl_tipImgCount',\r\n tipImgPh: 'crl_tipImgPh',\r\n tipBadge: 'crl_tipBadge',\r\n favStar: 'crl_favStar',\r\n favItem: 'crl_favItem',\r\n favToggle: 'crl_favToggle',\r\n favToggleLabel: 'crl_favToggleLabel',\r\n}\r\n\r\n// ---- data helpers ----\r\nconst NOOP_STORE = { getSnapshot: () => undefined, subscribe: () => () => {} }\r\n\r\n// ---- favorites (persisted per-session bookmark store) ----\r\n// Shape: Record<sessionId, messageId[]>. The durable message id is the same\r\n// identity the host projection emits (`id`) and the DOM anchor encodes\r\n// (`13:input-message<id>`), so DOM-injected buttons and the rail agree on\r\n// the key with no projection hop.\r\n// 持久化(2026-08-27):host 文件(~/.dsh/chat-rail-favorites.json,经\r\n// /chat-rail/api/favorites GET/PUT)——localStorage 按 origin 隔离,思灵 DSH\r\n// web 端口每次启动随机 → 跨重启丢弃。localStorage 仅作旧数据一次性迁移。\r\nconst FAVORITES_KEY = '@max-null/dsh-chat-rail:favorites'\r\ntype FavoritesMap = Record<string, string[]>\r\n\r\nconst FAVORITES_API = '/chat-rail/api/favorites'\r\n/** 模块级快照缓存(host 加载后充当源 truth)+ 订阅者集合。 */\r\nlet favoritesCache: FavoritesMap | null = null\r\nconst favoritesListeners = new Set<() => void>()\r\n\r\n/** 从 localStorage 读旧数据(迁移用/离线兜底)。 */\r\nfunction readFavoritesLocal(): FavoritesMap {\r\n if (typeof localStorage === 'undefined') return {}\r\n try {\r\n const raw = localStorage.getItem(FAVORITES_KEY)\r\n if (raw === null) return {}\r\n const value: unknown = JSON.parse(raw)\r\n return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as FavoritesMap : {}\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\nasync function fetchFavoritesMap(): Promise<FavoritesMap> {\r\n try {\r\n const res = await fetch(FAVORITES_API, { headers: { Accept: 'application/json' } })\r\n if (!res.ok) return {}\r\n const data = await res.json() as { ok?: boolean, value?: unknown }\r\n const value = data?.value\r\n return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as FavoritesMap : {}\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\n/** 模块加载时同步 host;旧 localStorage 数据迁移一次(host 为空且本地有值)。 */\r\nvoid (async () => {\r\n const host = await fetchFavoritesMap()\r\n if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {\r\n const legacy = readFavoritesLocal()\r\n if (Object.keys(host).length === 0 && Object.keys(legacy).length > 0) {\r\n await fetch(FAVORITES_API, {\r\n method: 'PUT', headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ favorites: legacy }),\r\n }).catch(() => {})\r\n }\r\n try { localStorage.removeItem(FAVORITES_KEY) } catch { /* 忽略 */ }\r\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\r\n favoritesCache = Object.keys(host).length === 0 ? legacy : host\r\n // host 就位后通知快照订阅(rail/收藏按钮重渲染)\r\n for (const listener of favoritesListeners) listener()\r\n } else {\r\n favoritesCache = host\r\n }\r\n})()\r\n\r\n/** Read the favorites map(内存镜像;未加载完成时回退 localStorage)。 */\r\nexport function readFavorites(): FavoritesMap {\r\n if (favoritesCache !== null) return favoritesCache\r\n return readFavoritesLocal()\r\n}\r\n\r\n/** Write the favorites map;更新内存并 fire-and-forget 持久化到 host(host-only)。 */\r\nfunction writeFavorites(map: FavoritesMap): void {\r\n favoritesCache = map\r\n void fetch(FAVORITES_API, {\r\n method: 'PUT', headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ favorites: map }),\r\n }).catch(() => { /* 网络失败:内存仍在用(下次写会重试) */ })\r\n}\r\n\r\n/** Normalized id list for one session (drops non-string entries). */\r\nexport function favoriteIdsOf(map: FavoritesMap, sessionId: string): string[] {\r\n const list = map[sessionId]\r\n return Array.isArray(list) ? list.filter((id): id is string => typeof id === 'string') : []\r\n}\r\n\r\n/** Toggle one id in a list (stable new array; no in-place mutation). */\r\nexport function toggleFavoriteId(list: string[], id: string): string[] {\r\n return list.includes(id) ? list.filter((candidate) => candidate !== id) : [...list, id]\r\n}\r\n\r\n/** Whether one message is favorited in one session. */\r\nexport function isFavorite(map: FavoritesMap, sessionId: string, messageId: string): boolean {\r\n return favoriteIdsOf(map, sessionId).includes(messageId)\r\n}\r\n\r\n/** Stable snapshot for useSyncExternalStore (loaded once, cached until toggle). */\r\nfunction favoritesSnapshot(): FavoritesMap {\r\n if (favoritesCache === null) favoritesCache = readFavorites()\r\n return favoritesCache\r\n}\r\n\r\n/** Subscribe to favorites changes; returns the unsubscribe. */\r\nfunction subscribeFavorites(listener: () => void): () => void {\r\n favoritesListeners.add(listener)\r\n return () => { favoritesListeners.delete(listener) }\r\n}\r\n\r\n/** Toggle one message's favorite state: snapshot → new map → persist → notify. */\r\nfunction toggleFavorite(sessionId: string, messageId: string): void {\r\n const next = { ...favoritesSnapshot() }\r\n next[sessionId] = toggleFavoriteId(favoriteIdsOf(favoritesSnapshot(), sessionId), messageId)\r\n favoritesCache = next\r\n writeFavorites(next)\r\n for (const listener of [...favoritesListeners]) listener()\r\n}\r\n\r\n/** Remember the messageId read off a DOM anchor key (`13:input-message<id>`). */\r\nexport function messageIdOfAnchorKey(key: string): string {\r\n return key.startsWith('13:input-message') ? key.slice('13:input-message'.length) : key\r\n}\r\n\r\n/** The favorite key of one rail message: the durable id when present (host\r\n * projection), otherwise the id reconstructed from the node anchor (the\r\n * loaded-node fallback path). */\r\nfunction favoriteIdOfMessage(m: Pick<RailMessage, 'id' | 'key'>): string {\r\n if (typeof m.id === 'string' && m.id !== '') return m.id\r\n if (typeof m.key === 'string') return messageIdOfAnchorKey(m.key)\r\n return ''\r\n}\r\n\r\n// ---- message-action injector (DOM) ----\r\n// User-message rows have no official action slot (assistant-actions serves\r\n// assistant turns only), so the star (favorite) and plus (fill-to-input)\r\n// buttons are injected next to the copy button of each user row. The rows\r\n// are located by their anchor key so history paging and re-renders both\r\n// land; the injector is a MutationObserver that re-scans after any DOM\r\n// change and never double-injects (row marker).\r\n//\r\n// Runtime wiring lives in a module-level context the rail component updates\r\n// every render (session id + handlers), so the pure DOM buttons stay tiny:\r\n// they only read data attributes and call context handlers.\r\nconst actionCtx: {\r\n sessionId: string | undefined\r\n onToggleFavorite: (messageId: string) => void\r\n onFill: (messageId: string) => void\r\n} = {\r\n sessionId: undefined,\r\n onToggleFavorite: () => {},\r\n onFill: () => {},\r\n}\r\n\r\n/** aria-label values of the copy button across shipped locales (button sits\r\n * in the user row's IconActions; its parent is the row's action host). */\r\nconst COPY_ARIA_LABELS = new Set(['复制', 'Copy', '已复制', 'Copied'])\r\n\r\n/** The copy button inside one user message row (null until rendered). */\r\nfunction copyButtonOf(row: HTMLElement): HTMLButtonElement | null {\r\n for (const button of row.querySelectorAll<HTMLButtonElement>('button')) {\r\n const label = button.getAttribute('aria-label')\r\n if (label !== null && COPY_ARIA_LABELS.has(label)) return button\r\n }\r\n return null\r\n}\r\n\r\n/** Star SVG path (milestone-compatible glyph, 24-unit viewBox). */\r\nconst STAR_PATH = 'm12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'\r\n\r\n/** Build the favorite (star) button for one user message row. */\r\nfunction favoriteButtonOf(messageId: string, lang: Record<string, string>): HTMLButtonElement {\r\n const button = document.createElement('button')\r\n button.type = 'button'\r\n button.className = 'crl_msgAct crl_fav'\r\n button.dataset.crlStar = messageId\r\n const initialLabel = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? lang.unfav : lang.fav\r\n button.setAttribute('aria-pressed', String(isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId)))\r\n button.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? 'true' : undefined\r\n button.classList.toggle('crl_on', isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId))\r\n // The CSS tooltip reads data-tip (a short dark pill like the host Tooltip);\r\n // the native title balloon is intentionally absent — double tips would\r\n // stack a browser bubble over the styled one.\r\n button.setAttribute('data-tip', initialLabel)\r\n button.setAttribute('aria-label', initialLabel)\r\n button.innerHTML = `<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"${STAR_PATH}\"/></svg>`\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault()\r\n event.stopPropagation()\r\n // Read the id off the dataset at click time: the injector may refresh the\r\n // button for a new session and the closure must stay current.\r\n const id = button.dataset.crlStar\r\n if (id !== undefined) {\r\n actionCtx.onToggleFavorite(id)\r\n refreshFavoriteButton(button, lang)\r\n }\r\n })\r\n return button\r\n}\r\n\r\n/** Build the fill-to-input (plus) button for one user message row. */\r\nfunction fillButtonOf(messageId: string, lang: Record<string, string>): HTMLButtonElement {\r\n const button = document.createElement('button')\r\n button.type = 'button'\r\n button.className = 'crl_msgAct crl_fill'\r\n button.dataset.crlFill = messageId\r\n button.setAttribute('data-tip', lang.fill)\r\n button.setAttribute('aria-label', lang.fill)\r\n button.innerHTML = '<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" aria-hidden=\"true\"><path d=\"M8 3.5v9M3.5 8h9\"/></svg>'\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault()\r\n event.stopPropagation()\r\n const id = button.dataset.crlFill\r\n if (id !== undefined) actionCtx.onFill(id)\r\n })\r\n return button\r\n}\r\n\r\n/** Sync one star button to its message's current favorite state. */\r\nfunction refreshFavoriteButton(button: HTMLButtonElement, lang: Record<string, string>): void {\r\n const id = button.dataset.crlStar\r\n if (id === undefined) return\r\n const starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', id)\r\n button.setAttribute('aria-pressed', String(starred))\r\n button.dataset.starred = starred ? 'true' : undefined\r\n button.classList.toggle('crl_on', starred)\r\n const label = starred ? lang.unfav : lang.fav\r\n // Both the a11y label and the styled tooltip follow the new state.\r\n button.setAttribute('data-tip', label)\r\n button.setAttribute('aria-label', label)\r\n}\r\n\r\n/** Refresh every injected star button (session switch / external toggle). */\r\nfunction refreshAllFavoriteButtons(lang: Record<string, string>): void {\r\n for (const button of document.querySelectorAll<HTMLButtonElement>('[data-crl-star]')) {\r\n refreshFavoriteButton(button, lang)\r\n }\r\n}\r\n\r\n/** Inject star + plus buttons into one user message row (idempotent). */\r\nfunction injectMessageActions(row: HTMLElement, lang: Record<string, string>): void {\r\n // Already injected and both buttons still present → leave it alone.\r\n if (row.dataset.crlActions === '1') {\r\n if (row.querySelector('[data-crl-star]') !== null && row.querySelector('[data-crl-fill]') !== null) return\r\n // React re-rendered the row and replaced the icons: fall through to re-inject.\r\n }\r\n const copy = copyButtonOf(row)\r\n const messageId = messageIdOfAnchorKey(row.getAttribute('data-chat-anchor-key') ?? '')\r\n if (copy === null || messageId === '') return\r\n const star = favoriteButtonOf(messageId, lang)\r\n star.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? 'true' : undefined\r\n star.classList.toggle('crl_on', isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId))\r\n const fill = fillButtonOf(messageId, lang)\r\n copy.before(star, fill)\r\n row.dataset.crlActions = '1'\r\n}\r\n\r\n/** Start observing new user rows so injected actions follow history paging. */\r\nfunction startActionInjector(lang: Record<string, string>): () => void {\r\n const scan = () => {\r\n for (const row of document.querySelectorAll<HTMLElement>('[data-chat-anchor-key^=\"13:input-message\"]')) {\r\n injectMessageActions(row, lang)\r\n }\r\n }\r\n const observer = new MutationObserver(scan)\r\n if (typeof document !== 'undefined' && document.body !== null) {\r\n observer.observe(document.body, { childList: true, subtree: true })\r\n }\r\n scan()\r\n return () => observer.disconnect()\r\n}\r\n\r\ninterface RailImage {\r\n attachmentId: string\r\n mediaType: string\r\n width: number\r\n height: number\r\n}\r\n\r\ninterface RailMessage {\r\n seq: number\r\n time: number\r\n text: string\r\n hasImage: boolean\r\n /** Stored-image references from the host projection (empty when inline-only). */\r\n images?: RailImage[]\r\n key?: string\r\n id?: string\r\n}\r\n\r\n/** One displayable tip image: either a durable reference (resolved lazily) or\r\n * an inline base64 payload already usable as an <img> src. */\r\ntype ImageSpec =\r\n | { kind: 'ref'; attachmentId: string; mediaType: string }\r\n | { kind: 'data'; src: string }\r\n\r\n/** Extract stored-image references from a ContentBlock list (reference form). */\r\nfunction nodeImagesOf(content: unknown): RailImage[] {\r\n if (!Array.isArray(content)) return []\r\n const out: RailImage[] = []\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a === null || typeof a !== 'object') continue\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown; width?: unknown; height?: unknown }\r\n if (typeof ref.attachmentId !== 'string' || ref.attachmentId === '') continue\r\n out.push({\r\n attachmentId: ref.attachmentId,\r\n mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png',\r\n width: typeof ref.width === 'number' ? ref.width : 0,\r\n height: typeof ref.height === 'number' ? ref.height : 0,\r\n })\r\n }\r\n return out\r\n}\r\n\r\n/** Convert a ContentBlock list to displayable image specs; inline base64\r\n * blocks (rare in replayed history) become data URLs on the spot. */\r\nfunction imageSpecsOfContent(content: unknown): ImageSpec[] {\r\n const specs: ImageSpec[] = []\r\n if (!Array.isArray(content)) return specs\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown; data?: unknown; mediaType?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a !== null && typeof a === 'object') {\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown }\r\n if (typeof ref.attachmentId === 'string' && ref.attachmentId !== '') {\r\n specs.push({ kind: 'ref', attachmentId: ref.attachmentId, mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png' })\r\n continue\r\n }\r\n }\r\n if (typeof b.data === 'string' && b.data !== '') {\r\n specs.push({ kind: 'data', src: `data:${typeof b.mediaType === 'string' ? b.mediaType : 'image/png'};base64,${b.data}` })\r\n }\r\n }\r\n return specs\r\n}\r\n\r\n/** Normalize one projection entry to a rail message. */\r\nfunction normalize(m: unknown): RailMessage | null {\r\n if (m === null || typeof m !== 'object') return null\r\n const o = m as Record<string, unknown>\r\n if (typeof o.seq !== 'number') return null\r\n // Host projection emits `text`; keep `preview` as a defensive fallback in\r\n // case an older projection payload is still cached in the browser.\r\n const text = typeof o.text === 'string' ? o.text : typeof o.preview === 'string' ? o.preview : ''\r\n const images = Array.isArray(o.images)\r\n ? o.images.map(nodeImagesOfEntry).filter((i): i is RailImage => i !== null)\r\n : undefined\r\n return {\r\n seq: o.seq,\r\n time: typeof o.time === 'number' ? o.time : 0,\r\n text,\r\n // Older projections lack hasImage; defensive default false.\r\n hasImage: o.hasImage === true,\r\n ...(images !== undefined && images.length > 0 ? { images } : {}),\r\n ...(typeof o.key === 'string' ? { key: o.key } : {}),\r\n ...(typeof o.id === 'string' ? { id: o.id } : {}),\r\n }\r\n}\r\n\r\n/** Normalize one projection entry of the `images` array (wire form). */\r\nfunction nodeImagesOfEntry(v: unknown): RailImage | null {\r\n if (v === null || typeof v !== 'object') return null\r\n const o = v as Record<string, unknown>\r\n if (typeof o.attachmentId !== 'string' || o.attachmentId === '') return null\r\n return {\r\n attachmentId: o.attachmentId,\r\n mediaType: typeof o.mediaType === 'string' ? o.mediaType : 'image/png',\r\n width: typeof o.width === 'number' ? o.width : 0,\r\n height: typeof o.height === 'number' ? o.height : 0,\r\n }\r\n}\r\n\r\n/** Extract preview text from a user message's ContentBlock list. */\r\nfunction userTextOf(content: unknown): string {\r\n if (!Array.isArray(content)) return ''\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n return out.trim().slice(0, 80)\r\n}\r\n\r\n/** Whether a ContentBlock list carries an image block (rc.8 attachments). */\r\nfunction userHasImage(content: unknown): boolean {\r\n if (!Array.isArray(content)) return false\r\n return content.some(block => block !== null && typeof block === 'object'\r\n && (block as { type?: unknown }).type === 'image')\r\n}\r\n\r\n// ---- Chat-node data plane (DSH 0.1.2-alpha.2+) ----\r\n// DSH 0.1.2-alpha.2 moved Conversation target data out of the Session\r\n// snapshot: `session.getSnapshot()` no longer carries `chat.nodes`, and the\r\n// per-Session Chat nodes live in the uiConversation `'chat'` view target\r\n// (ChatSnapshot with a keyed `nodes` store). The rail resolves nodes from\r\n// either plane — current DSH first, legacy Session snapshot as fallback.\r\n\r\n/** Observable snapshot surface of one Session's `'chat'` view target. */\r\nexport interface ChatViewSource {\r\n subscribe(listener: () => void): () => void\r\n getSnapshot(): unknown\r\n}\r\n\r\n/** Keyed Chat node store published by the official ChatSnapshot (`nodes`). */\r\ninterface ChatNodeStoreLike {\r\n get(key: string): unknown\r\n values(): Iterable<unknown> | readonly unknown[]\r\n}\r\n\r\n/** Structural shape of the uiConversation `'chat'` target snapshot. */\r\ninterface ChatSnapshotLike {\r\n readonly nodes: ChatNodeStoreLike\r\n}\r\n\r\n/** Minimal observable shape of a chat node (either data plane). */\r\ninterface ChatNodeLike {\r\n readonly key?: unknown\r\n readonly kind?: unknown\r\n readonly anchorSeq?: unknown\r\n readonly data?: { time?: unknown; content?: unknown } | null\r\n}\r\n/** Runtime face of the official Conversation assembly service\r\n * (DSH 0.1.2-alpha.2+). Kept as a local structural type because the npm\r\n * client-runtime types (0.1.1-rc.1) predate the service rename and still\r\n * expose the legacy registry names. */\r\ninterface UiConversationRuntime {\r\n binding(sessionId: string): {\r\n target(name: 'chat'): ChatViewSource | undefined\r\n }\r\n}\r\n\r\n/**\r\n * Resolve one chat node from either data plane: the official Chat view\r\n * snapshot (`nodes` keyed store) on current DSH, or the legacy Session\r\n * snapshot's `chat.nodes` Map on older DSH.\r\n * @param snapshot - the active node snapshot.\r\n * @param key - the Conversation Context key.\r\n * @returns the raw node, when its window covers the key.\r\n */\r\nexport function chatNodeOf(snapshot: unknown, key: string | undefined): unknown {\r\n if (key === undefined || snapshot === undefined || snapshot === null) return undefined\r\n const store = (snapshot as ChatSnapshotLike | undefined)?.nodes as ChatNodeStoreLike | undefined\r\n if (store !== undefined && typeof store.get === 'function') {\r\n return store.get(key)\r\n }\r\n const map = (snapshot as { chat?: { nodes?: Map<string, unknown> } } | undefined)?.chat?.nodes\r\n return map?.get(key)\r\n}\r\n\r\n/** Map one chat node (either plane) to a rail message entry. */\r\nexport function railMessageOfNode(node: unknown): RailMessage | null {\r\n if (node === null || typeof node !== 'object') return null\r\n const n = node as ChatNodeLike\r\n if (n.kind !== 'user' && n.kind !== 'steering') return null\r\n const key = typeof n.key === 'string' ? n.key : undefined\r\n if (key === undefined) return null\r\n const data = n.data\r\n if (data === null || typeof data !== 'object') return null\r\n if (typeof data.time !== 'number' || !Array.isArray(data.content)) return null\r\n const images = nodeImagesOf(data.content)\r\n return {\r\n seq: typeof n.anchorSeq === 'number' ? n.anchorSeq : 0,\r\n time: data.time,\r\n text: userTextOf(data.content),\r\n hasImage: userHasImage(data.content),\r\n ...(images.length > 0 ? { images } : {}),\r\n key,\r\n }\r\n}\r\n\r\n/** Fallback collector: enumerate user messages from the loaded chat nodes.\r\n * Accepts either data plane — the official Chat view snapshot (`nodes`\r\n * values) or the legacy Session snapshot's `chat.nodes` Map. */\r\nexport function collectFromNodes(snapshot: unknown): RailMessage[] {\r\n const out: RailMessage[] = []\r\n const store = (snapshot as ChatSnapshotLike | undefined)?.nodes as ChatNodeStoreLike | undefined\r\n if (store !== undefined && typeof store.values === 'function') {\r\n for (const node of store.values()) {\r\n const m = railMessageOfNode(node)\r\n if (m !== null) out.push(m)\r\n }\r\n } else {\r\n const chat = (snapshot as { chat?: { nodes?: Map<unknown, unknown> } } | undefined)?.chat\r\n if (chat?.nodes === undefined) return out\r\n for (const node of chat.nodes.values()) {\r\n const m = railMessageOfNode(node)\r\n if (m !== null) out.push(m)\r\n }\r\n }\r\n out.sort((a, b) => a.seq - b.seq)\r\n return out\r\n}\r\n\r\n/** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */\r\nfunction anchorKeyOf(m: RailMessage): string | undefined {\r\n if (typeof m.key === 'string' && m.key !== '') return m.key\r\n if (typeof m.id === 'string' && m.id !== '') return '13:input-message' + m.id\r\n return undefined\r\n}\r\n\r\n/** Full text of a rail message from the loaded chat nodes (uncapped),\r\n * falling back to the projection preview when the node is not mounted. */\r\nfunction fullTextOf(m: RailMessage, nodeSnapshot: unknown): string {\r\n const key = anchorKeyOf(m)\r\n const node = chatNodeOf(nodeSnapshot, key) as { data?: { content?: unknown } } | undefined\r\n const content = node?.data?.content\r\n if (Array.isArray(content)) {\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n const full = out.trim()\r\n if (full !== '') return full\r\n }\r\n return m.text\r\n}\r\n\r\n/** Displayable tip images for one rail message: host projection references\r\n * first (works even before the node window covers the message), then the\r\n * loaded chat node's content blocks. */\r\nfunction tipImagesOf(m: RailMessage, nodeSnapshot: unknown): ImageSpec[] {\r\n if (Array.isArray(m.images) && m.images.length > 0) {\r\n return m.images.map(img => ({ kind: 'ref', attachmentId: img.attachmentId, mediaType: img.mediaType }))\r\n }\r\n const key = anchorKeyOf(m)\r\n const node = chatNodeOf(nodeSnapshot, key) as { data?: { content?: unknown } } | undefined\r\n return imageSpecsOfContent(node?.data?.content)\r\n}\r\n\r\nconst delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\r\n\r\n/**\r\n * Ensure the message node is loaded into the visible window, then scroll to it.\r\n *\r\n * DSH 0.1.2-alpha.3+ (session-controller) pages history through the exact\r\n * `loadThrough(seq)` jump pager (documented as the turn-jump loader, 200\r\n * messages per page): when the message's event `seq` is known the rail jumps\r\n * with one precise `loadThrough` call and settles its completion, instead of\r\n * paging `loadOlder` (50 messages per page, DSH PAGE_MESSAGES) from the window\r\n * head — a far jump in a long session costs one call, not dozens.\r\n *\r\n * `loadThrough` returns immediately when another loader owns the busy flag\r\n * (the official caller retries once it settles), so the rail waits for the\r\n * owner to release before issuing the jump; after it resolves, the chat view\r\n * assembles the paged window asynchronously, so the target node is polled\r\n * before the row, and the row before the scroll.\r\n *\r\n * Fallback (older DSH without `loadThrough`, or a message without a dependable\r\n * seq): the `loadOlder` loop below, with its page-count progress. \"Loaded\" is\r\n * decided through `nodeOf` — the uiConversation `'chat'` target on current\r\n * DSH, where the Session snapshot no longer carries chat.nodes — falling back\r\n * to the legacy Session snapshot's `chat.nodes` Map without it.\r\n */\r\nasync function jumpToMessage(\r\n sessionsService: { binding: (id: string) => { session?: { getSnapshot(): unknown; loadOlder(): Promise<unknown>; hasMore?: boolean; loadingOlder?: boolean } } | undefined },\r\n sessionId: string,\r\n key: string,\r\n /** Live chat-node reader: called with the current snapshot on every probe. */\r\n nodeOf?: (key: string) => unknown,\r\n onProgress?: (pages: number) => void,\r\n signal?: AbortSignal,\r\n /** Durable event seq of the target message (chatRail projection / node anchor seq). */\r\n targetSeq?: number,\r\n): Promise<boolean> {\r\n const session = sessionsService.binding(sessionId)?.session\r\n if (session === undefined) return false\r\n const isLoaded = nodeOf === undefined\r\n ? (k: string) => (session.getSnapshot() as { chat?: { nodes?: Map<string, unknown> } }).chat?.nodes?.get(k) !== undefined\r\n : nodeOf\r\n let pages = 0\r\n let loaded = false\r\n const jumpLoadThrough = (session as { loadThrough?: ((seq: number) => Promise<unknown>) | undefined }).loadThrough\r\n if (typeof jumpLoadThrough === 'function' && targetSeq !== undefined && Number.isSafeInteger(targetSeq)) {\r\n // A plain pull owns the busy flag during its single page; loadThrough does\r\n // not queue behind it, so wait for the owner instead of returning a false\r\n // \"done\" on the first probe.\r\n let spin = 0\r\n while (spin++ < 120) {\r\n if (signal?.aborted) return false\r\n const snapshot = session.getSnapshot() as { loadingOlder?: boolean } | undefined\r\n if (snapshot?.loadingOlder !== true) break\r\n await delay(50)\r\n }\r\n if (signal?.aborted) return false\r\n await jumpLoadThrough(targetSeq)\r\n // The pager settles when the window covers targetSeq (or history is\r\n // exhausted), but the chat view assembles the paged window asynchronously:\r\n // poll the node before concluding, bounded the same way as the DOM poll.\r\n let spinNode = 0\r\n while (spinNode++ < 100) {\r\n if (signal?.aborted) return false\r\n if (isLoaded(key)) { loaded = true; break }\r\n await delay(50)\r\n }\r\n if (!loaded) {\r\n console.warn(`[chat-rail] jumpToMessage: node \"${key}\" not loaded after loadThrough(${String(targetSeq)})`)\r\n return false\r\n }\r\n } else {\r\n let guard = 0\r\n while (guard++ < 120) {\r\n if (signal?.aborted) return false\r\n const snapshot = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean } | undefined\r\n if (isLoaded(key)) {\r\n loaded = true\r\n break\r\n }\r\n if (snapshot?.hasMore !== true) break\r\n if (snapshot.loadingOlder === true) {\r\n // Another loader owns the current page; wait for it without busy-spinning.\r\n await delay(50)\r\n continue\r\n }\r\n await session.loadOlder()\r\n pages++\r\n // Report progress only on page boundaries, not every loop iteration.\r\n onProgress?.(pages)\r\n }\r\n // Page guard exhausted (or hasMore ran out) without the node materializing:\r\n // fail fast with context instead of burning the DOM poll below.\r\n if (!loaded) {\r\n console.warn(`[chat-rail] jumpToMessage: node \"${key}\" not loaded after ${pages} page(s)`)\r\n return false\r\n }\r\n }\r\n const scrollport = typeof document !== 'undefined' ? document.querySelector('[data-conversation-scroll]') : null\r\n if (scrollport === null) return false\r\n // The snapshot is authoritative for \"loaded\", but the row's DOM node only\r\n // appears once React renders the prepended page. Poll for it (up to ~5s) so a\r\n // freshly paged-in jump scrolls on the same click.\r\n let row: Element | null = null\r\n let waited = 0\r\n while (waited++ < 100) {\r\n if (signal?.aborted) return false\r\n row = scrollport.querySelector(`[data-chat-anchor-key=\"${CSS.escape(key)}\"]`)\r\n if (row !== null) break\r\n await delay(50)\r\n }\r\n if (row === null) return false\r\n const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\r\n // DSH 0.1.2-alpha.3+ paging compensates the reader position while the newly\r\n // prepended window re-flows — a scrollIntoView issued right after a multi-\r\n // page loadThrough lands mid-flight and gets overridden by the compensation\r\n // (verified on the alpha.4 kernel: 145971 → 44464 snap-back). Wait for the\r\n // scrollport geometry to go quiet, land with an explicit scrollTo computed\r\n // from the row's viewport rect, then re-verify a few times: a stray\r\n // bottom-follow can still pull the flight back.\r\n const scrollToRow = (): void => {\r\n const viewRect = scrollport.getBoundingClientRect()\r\n const rowRect = (row as Element).getBoundingClientRect()\r\n const target = scrollport.scrollTop + (rowRect.top - viewRect.top) - (viewRect.height - rowRect.height) / 2\r\n if (target < 0) return\r\n scrollport.scrollTo({ top: target, behavior: reducedMotion ? 'auto' : 'smooth' })\r\n }\r\n const rowDelta = (): number => {\r\n const viewRect = scrollport.getBoundingClientRect()\r\n const rowRect = (row as Element).getBoundingClientRect()\r\n return Math.abs((rowRect.top + rowRect.height / 2) - (viewRect.top + viewRect.height / 2))\r\n }\r\n let stableTop = -1\r\n let stableHeight = -1\r\n let stability = 0\r\n let quiet = 0\r\n while (quiet++ < 30) {\r\n if (signal?.aborted) return false\r\n const top = scrollport.scrollTop\r\n const height = scrollport.scrollHeight\r\n if (top === stableTop && height === stableHeight) stability++\r\n else { stableTop = top; stableHeight = height; stability = 0 }\r\n if (stability >= 3) break\r\n await delay(150)\r\n }\r\n if (signal?.aborted) return false\r\n scrollToRow()\r\n for (let attempt = 0; attempt < 4; attempt++) {\r\n await delay(600)\r\n if (signal?.aborted) return false\r\n if (rowDelta() <= Math.max(120, scrollport.getBoundingClientRect().height * 0.2)) break\r\n scrollToRow()\r\n }\r\n return true\r\n}\r\n\r\n// ---- tip image thumbnails ----\r\n\r\n/** Session-scoped browser URL cache for resolved attachment bytes (one entry\r\n * per session:attachment; a failed load evicts itself so the next hover\r\n * retries instead of showing a permanently broken thumbnail). */\r\nconst thumbUrls = new Map<string, Promise<string>>()\r\n\r\n/** Minimal RpcResult shape of session.readAttachment (contract level). */\r\ninterface ReadAttachmentResult {\r\n ok: boolean\r\n value?: { attachment?: { mediaType?: string }; data?: Uint8Array }\r\n error?: { code?: string; message?: string }\r\n}\r\n\r\ntype ReadAttachmentFn = (attachmentId: string) => Promise<ReadAttachmentResult>\r\n\r\n/** Fallback data URL when the browser cannot mint object URLs. */\r\nfunction bytesToDataUrl(data: Uint8Array, mediaType: string): string {\r\n let binary = ''\r\n const chunk = 0x8000\r\n for (let offset = 0; offset < data.length; offset += chunk) {\r\n binary += String.fromCharCode(...data.subarray(offset, offset + chunk))\r\n }\r\n return `data:${mediaType};base64,${btoa(binary)}`\r\n}\r\n\r\n/** Resolve (and cache) one stored image to a displayable URL. */\r\nfunction resolveThumb(sessionId: string, read: ReadAttachmentFn, image: Pick<RailImage, 'attachmentId' | 'mediaType'>): Promise<string> {\r\n const key = `${sessionId}:${image.attachmentId}`\r\n let pending = thumbUrls.get(key)\r\n if (pending === undefined) {\r\n pending = read(image.attachmentId).then((result): string => {\r\n if (!result.ok || result.value === undefined) {\r\n throw new Error(result.error?.message ?? result.error?.code ?? 'readAttachment failed')\r\n }\r\n const data = result.value.data\r\n if (data === undefined) throw new Error('readAttachment resolved no bytes')\r\n const mediaType = result.value.attachment?.mediaType ?? image.mediaType\r\n if (typeof URL.createObjectURL === 'function') {\r\n return URL.createObjectURL(new Blob([data as BlobPart], { type: mediaType }))\r\n }\r\n return bytesToDataUrl(data, mediaType)\r\n })\r\n void pending.catch(() => { thumbUrls.delete(key) })\r\n thumbUrls.set(key, pending)\r\n }\r\n return pending\r\n}\r\n\r\n/** One tip thumbnail: resolves its attachment lazily, shows a pulsing\r\n * placeholder while loading, and removes itself on failure. */\r\nfunction TipThumb({ spec, sessionId, read }: {\r\n spec: ImageSpec\r\n sessionId: string\r\n read: ReadAttachmentFn\r\n}): ReactNode {\r\n const [src, setSrc] = useState<string | null>(null)\r\n const [failed, setFailed] = useState(false)\r\n const specKey = spec.kind === 'ref' ? `ref:${spec.attachmentId}` : 'data'\r\n useEffect(() => {\r\n let alive = true\r\n setSrc(null)\r\n setFailed(false)\r\n if (spec.kind === 'data') {\r\n setSrc(spec.src)\r\n return () => { alive = false }\r\n }\r\n resolveThumb(sessionId, read, spec)\r\n .then((url) => { if (alive) setSrc(url) })\r\n .catch(() => { if (alive) setFailed(true) })\r\n return () => { alive = false }\r\n // spec is a fresh object each render; the string key is the stable identity.\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [specKey, sessionId, read])\r\n if (failed) return null\r\n if (src === null) return createElement('div', { className: S.tipImgPh, 'aria-hidden': true })\r\n return createElement('img', { className: S.tipImg, src, alt: '', draggable: false })\r\n}\r\n\r\n// ---- component ----\r\ninterface TimelineRailProps {\r\n useProjection: (key: string) => { messages?: unknown[] } | undefined\r\n sessionId?: SessionId\r\n sessionsService: ISessions\r\n /** Live Chat view source for the session (current DSH `'chat'` target). */\r\n chatOf?: (sessionId: string) => ChatViewSource | undefined\r\n /** Draft write + attachment-add face (session-scope framework injection). */\r\n inputActions?: {\r\n setDraft(text: string): void\r\n addImages(ids: readonly string[]): boolean\r\n } | undefined\r\n /** Runtime draft-image registry (hosted by ui-conversation). */\r\n conversation?: {\r\n createDraftImages(files: readonly File[]): readonly { id: string }[]\r\n } | undefined\r\n}\r\n\r\n/** Resolve copy for the current UI language (document lang, DSH sets zh/en). */\r\nfunction langStrings(): Record<string, string> {\r\n const lang = typeof document !== 'undefined' ? (document.documentElement.lang || 'zh').toLowerCase() : 'zh'\r\n return STRINGS[lang.startsWith('zh') ? 'zh' : 'en']\r\n}\r\n\r\nfunction TimelineRail({ useProjection, sessionId, sessionsService, chatOf, inputActions, conversation }: TimelineRailProps): ReactNode {\r\n const t = langStrings()\r\n const projected = useProjection('chatRail')\r\n const session = sessionId === undefined ? undefined : (sessionsService.binding(sessionId) as { session?: { subscribe(cb: () => void): () => void; getSnapshot(): unknown } } | undefined)?.session\r\n // Chat-node source: the official `'chat'` view target on current DSH (the\r\n // Session snapshot no longer carries chat.nodes); the Session snapshot stays\r\n // as the legacy fallback on older DSH.\r\n const chatSource = sessionId === undefined ? undefined : chatOf?.(sessionId)\r\n const fallbackStore = chatSource ?? (session === undefined ? NOOP_STORE : session)\r\n const nodeSnapshot = useSyncExternalStore(\r\n (cb) => fallbackStore.subscribe(cb),\r\n () => fallbackStore.getSnapshot(),\r\n )\r\n\r\n let messages: RailMessage[] = []\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) {\r\n messages = projected.messages.map(normalize).filter((m): m is RailMessage => m !== null)\r\n }\r\n if (messages.length === 0) {\r\n messages = collectFromNodes(nodeSnapshot)\r\n }\r\n\r\n // 「使用官方轮次导航条」(对比模式,2026-09-05):默认关(chat-rail 主导、隐藏官方);打开 =\r\n // 官方 TurnNavigator 显示 + 本 rail 隐藏 + 行内按钮清理。设置来自官方 settingsScope(设置——\r\n // 插件页卡片),切换即时生效;rail 与卡片订阅同一 scope。\r\n const railSettingsSnap = useSyncExternalStore(\r\n railSettingsScope !== null ? (cb: () => void) => railSettingsScope!.subscribe(cb) : NOOP_SUB,\r\n () => railSettingsScope?.getSnapshot() ?? { value: undefined },\r\n )\r\n const showOfficial = railSettingsSnap.value?.showOfficialNavigator === true\r\n const applyOfficialMode = (on: boolean): void => {\r\n syncOfficialHide(on)\r\n if (on) {\r\n clearRowActions()\r\n if (navRef.current !== null) navRef.current.style.display = 'none'\r\n } else if (navRef.current !== null) {\r\n navRef.current.style.display = ''\r\n }\r\n }\r\n useEffect(() => { applyOfficialMode(showOfficial) }, [showOfficial])\r\n\r\n // Favorites: per-session persisted set + a \"bookmarks only\" filter that\r\n // narrows the rail to favorited messages (mirrors dsh-milestone's\r\n // bookmarksOnly toggle; the DOM-injected buttons share the same store).\r\n const favorites = useSyncExternalStore(subscribeFavorites, favoritesSnapshot)\r\n const [favOnly, setFavOnly] = useState(false)\r\n // Count of favorited messages in THIS session. The filter toggle is only\r\n // worth showing when a favorite exists — with zero favorites there is\r\n // nothing to filter to, so the pill disappears entirely.\r\n const favCount = sessionId === undefined\r\n ? 0\r\n : messages.filter((m) => isFavorite(favorites, sessionId, favoriteIdOfMessage(m))).length\r\n const showFavToggle = favCount > 0\r\n const effectiveMessages = favOnly\r\n ? messages.filter((m) => sessionId !== undefined && isFavorite(favorites, sessionId, favoriteIdOfMessage(m)))\r\n : messages\r\n\r\n // If the filter is armed and the last favorite is un-starred (or the\r\n // session switched), the filtered list would be empty AND the toggle would\r\n // vanish — leaving a rail with nothing to un-filter. Auto-release the\r\n // filter instead so the full list returns.\r\n useEffect(() => {\r\n if (favOnly && !showFavToggle) setFavOnly(false)\r\n }, [favOnly, showFavToggle])\r\n\r\n const [activeIndex, setActiveIndex] = useState(-1)\r\n const [show, setShow] = useState(false)\r\n const [jumping, setJumping] = useState(false)\r\n // Hover full-content panel: index of the hovered item + fixed position.\r\n // The tip only triggers after the expand animation settles — in the\r\n // collapsed state the item rect is only 36px wide, so a tip positioned\r\n // there would be wrong once the rail expands.\r\n const [tip, setTip] = useState<{ index: number; x: number; y: number } | null>(null)\r\n const navRef = useRef<HTMLDivElement | null>(null) // True only after the expand animation has fully settled; the width\r\n // transition takes ~250ms after `show` flips, and item rects are only\r\n // stable once it finishes. Tip positioning must wait for this.\r\n const expandedRef = useRef(false)\r\n // Last observed pointer position over the rail. A mouseenter that arrives\r\n // before the width transition cannot show the tip (rects unstable), yet the\r\n // enter event is already gone by the time the animation ends — the settle\r\n // handler re-hit-tests the cursor against this position to show the tip for\r\n // the node still under the pointer.\r\n const lastPointerRef = useRef<{ x: number; y: number } | null>(null)\r\n // Aborts an in-flight jump (component unmount or a newer click superseding\r\n // an older one), so the loadOlder/DOM-poll loops stop promptly.\r\n const jumpAbortRef = useRef<AbortController | null>(null)\r\n useEffect(() => () => jumpAbortRef.current?.abort(), [])\r\n\r\n // Track the cursor over the rail for the expand-settled tip re-hit-test\r\n // (the `show` effect's settle handler reads lastPointerRef). Native\r\n // listener: plain DOM events keep the callback typed without React\r\n // synthetic-event generics in the hand-written createElement props.\r\n useEffect(() => {\r\n const el = navRef.current\r\n if (el === null) return\r\n const onMove = (e: PointerEvent) => { lastPointerRef.current = { x: e.clientX, y: e.clientY } }\r\n el.addEventListener('pointermove', onMove, { passive: true })\r\n return () => el.removeEventListener('pointermove', onMove)\r\n }, [])\r\n\r\n /** Position the tip against the item's CURRENT (post-expand) rect. */\r\n const positionTip = (index: number) => {\r\n if (index < 0) return\r\n const el = navRef.current?.querySelector<HTMLElement>(`[data-crl-index=\"${index}\"]`)\r\n if (el === null || el === undefined) return\r\n const rect = el.getBoundingClientRect()\r\n setTip({ index, x: rect.left - 12, y: rect.top })\r\n }\r\n\r\n /** Handle item hover: the collapsed state only expands the rail; the tip\r\n * only triggers once the expand animation has settled (item rects are then\r\n * stable, so the tip is positioned correctly). */\r\n const handleItemEnter = (index: number) => {\r\n if (!expandedRef.current) {\r\n setShow(true)\r\n return\r\n }\r\n positionTip(index)\r\n }\r\n\r\n const handleItemLeave = (index: number) => {\r\n setTip((prev) => (prev?.index === index ? null : prev))\r\n }\r\n\r\n // Track the settled expand state: when `show` flips true, wait for the width\r\n // transition to end before allowing tip triggers; when it flips false, the\r\n // rail is collapsing — treat as unexpanded immediately.\r\n useEffect(() => {\r\n if (!show) {\r\n expandedRef.current = false\r\n return\r\n }\r\n const el = navRef.current\r\n if (el === null) return\r\n let settled = false\r\n const settle = () => {\r\n if (settled) return\r\n settled = true\r\n expandedRef.current = true\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n // Re-hit-test the cursor: a mouseenter that fired before the width\r\n // transition could not show the tip (rects were still unstable), and\r\n // the enter event is long gone once the animation ends. If the pointer\r\n // still sits on a rail item, position that item's tip now.\r\n const p = lastPointerRef.current\r\n if (p !== null) {\r\n const hit = document.elementFromPoint(p.x, p.y)\r\n const item = hit === null ? null : hit.closest('[data-crl-index]')\r\n const idx = item === null ? -1 : Number(item.getAttribute('data-crl-index'))\r\n if (Number.isInteger(idx) && idx >= 0) positionTip(idx)\r\n }\r\n }\r\n const onTransitionEnd = (e: TransitionEvent) => {\r\n if (e.propertyName !== 'width') return\r\n settle()\r\n }\r\n el.addEventListener('transitionend', onTransitionEnd)\r\n // Fallback for `prefers-reduced-motion: reduce` (the stylesheet disables\r\n // the width transition, so `transitionend` never fires) and for any lost\r\n // event: settle after the transition duration regardless.\r\n const timer = setTimeout(settle, 300)\r\n return () => {\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n }\r\n }, [show])\r\n\r\n // Session-scoped attachment reader for tip thumbnails (stable identity so\r\n // TipThumb effects do not re-run every render; resolveThumb caches anyway).\r\n const readThumb = useMemo<ReadAttachmentFn | undefined>(() => {\r\n if (sessionId === undefined) return undefined\r\n const s = (sessionsService.binding(sessionId) as unknown as\r\n { session?: { readAttachment?: ReadAttachmentFn } } | undefined)?.session\r\n return s?.readAttachment === undefined\r\n ? undefined\r\n : (id: string) => s.readAttachment!(id)\r\n }, [sessionId, sessionsService])\r\n\r\n // Fill-to-input (plus button): put the message's full text into the draft\r\n // and re-attach its stored images through the official draft-image path\r\n // (readAttachment → File → createDraftImages → addImages). Degrades to\r\n // text-only when the conversation service or storage read is unavailable.\r\n const fillToInput = useCallback((messageId: string) => {\r\n const m = messages.find((candidate) => favoriteIdOfMessage(candidate) === messageId)\r\n if (m === undefined || inputActions === undefined || sessionId === undefined) return\r\n const text = fullTextOf(m, nodeSnapshot) || m.text\r\n if (inputActions.setDraft !== undefined) inputActions.setDraft(text)\r\n const specs = tipImagesOf(m, nodeSnapshot).filter((spec): spec is ImageSpec & { kind: 'ref' } => spec.kind === 'ref')\r\n if (specs.length === 0 || conversation === undefined || readThumb === undefined) return\r\n void (async () => {\r\n const files: File[] = []\r\n for (const spec of specs) {\r\n try {\r\n const result = await readThumb(spec.attachmentId) as unknown as { ok?: boolean; value?: { attachment?: { mediaType?: string }; data?: Uint8Array } }\r\n if (result?.ok !== true || result.value?.data === undefined) continue\r\n const mediaType = result.value.attachment?.mediaType ?? spec.mediaType\r\n files.push(new File([result.value.data as BlobPart], `attachment-${spec.attachmentId}${mediaType.includes('jpeg') ? '.jpg' : '.png'}`, { type: mediaType }))\r\n } catch {\r\n // One failed attachment must not block the others.\r\n }\r\n }\r\n if (files.length === 0) return\r\n const drafts = conversation.createDraftImages(files)\r\n const ids = drafts.map((draft) => draft.id).filter((id): id is string => id !== undefined)\r\n if (ids.length > 0) {\r\n try { inputActions.addImages?.(ids) } catch { /* draft-image registry may reject; text fill already landed */ }\r\n }\r\n })()\r\n }, [conversation, inputActions, messages, nodeSnapshot, readThumb, sessionId])\r\n\r\n // Bridge the DOM-injected buttons (module-level actionCtx) to this\r\n // component's live closures + session id; refresh star states on every\r\n // favorites change and on session switch.\r\n useEffect(() => {\r\n actionCtx.sessionId = sessionId\r\n actionCtx.onToggleFavorite = (messageId) => {\r\n if (sessionId !== undefined) toggleFavorite(sessionId, messageId)\r\n }\r\n actionCtx.onFill = fillToInput\r\n refreshAllFavoriteButtons(t)\r\n return () => {\r\n // Only clear what this render set: a newer session may already own the ctx.\r\n if (actionCtx.sessionId === sessionId) actionCtx.sessionId = undefined\r\n }\r\n }, [sessionId, fillToInput, t])\r\n\r\n // Keep the injected DOM buttons in sync with external favorite toggles\r\n // (rail star / another session's store write).\r\n useEffect(() => {\r\n refreshAllFavoriteButtons(t)\r\n }, [favorites, t])\r\n\r\n // Inject star + plus buttons into every user message row; observe DOM\r\n // changes so history paging and re-renders keep them present.\r\n useEffect(() => {\r\n if (typeof document === 'undefined' || document.body === null) return\r\n if (showOfficial) return\r\n if (typeof document !== 'undefined') {\r\n return startActionInjector(t)\r\n }\r\n }, [showOfficial, t])\r\n\r\n // Background full-history load: follow the runtime's authoritative hasMore\r\n // flag, but STOP as soon as the projection delivers.\r\n useEffect(() => {\r\n if (session === undefined) return\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n let cancelled = false\r\n const run = async () => {\r\n let guard = 0\r\n while (!cancelled && guard++ < 120) {\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n const snap = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean }\r\n if (snap?.hasMore !== true) return\r\n if (snap.loadingOlder === true) { await delay(50); continue }\r\n await (session as unknown as { loadOlder(): Promise<unknown> }).loadOlder()\r\n }\r\n }\r\n run().catch(() => {})\r\n return () => { cancelled = true }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, session === undefined ? 'none' : 'ready', Array.isArray(projected?.messages) && projected.messages.length > 0 ? 'have' : 'none'])\r\n\r\n // Track the reading position (active item): nearest row to the 40% viewport line.\r\n useEffect(() => {\r\n if (effectiveMessages.length === 0) return\r\n const indexByKey = new Map<string, number>()\r\n for (let i = 0; i < effectiveMessages.length; i++) {\r\n const key = anchorKeyOf(effectiveMessages[i])\r\n if (key !== undefined) indexByKey.set(key, i)\r\n }\r\n const updateActive = () => {\r\n const sp = document.querySelector('[data-conversation-scroll]')\r\n if (sp === null) return\r\n const rect = sp.getBoundingClientRect()\r\n if (rect.height === 0) return\r\n const line = rect.top + rect.height * 0.4\r\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"]')\r\n let best = -1\r\n let bestDist = Infinity\r\n for (const row of rows) {\r\n const key = row.getAttribute('data-chat-anchor-key')\r\n if (key === null) continue\r\n const idx = indexByKey.get(key) ?? -1\r\n if (idx === -1) continue\r\n const r = row.getBoundingClientRect()\r\n const dist = Math.abs(r.top + r.height / 2 - line)\r\n if (dist < bestDist) { bestDist = dist; best = idx }\r\n }\r\n setActiveIndex(best)\r\n }\r\n updateActive()\r\n const el = document.querySelector('[data-conversation-scroll]')\r\n let scrollTimer: ReturnType<typeof setTimeout> | null = null\r\n const onScroll = () => {\r\n if (scrollTimer !== null) return\r\n scrollTimer = setTimeout(() => { scrollTimer = null; updateActive() }, 60)\r\n }\r\n el?.addEventListener('scroll', onScroll, { passive: true })\r\n const timer = window.setInterval(updateActive, 2000)\r\n return () => {\r\n el?.removeEventListener('scroll', onScroll)\r\n window.clearInterval(timer)\r\n if (scrollTimer !== null) clearTimeout(scrollTimer)\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, effectiveMessages.length])\r\n\r\n // Scroll the rail so the active item stays centered in its visible area:\r\n // follows the conversation's reading position (and re-centers after a\r\n // session switch, where activeIndex is recomputed for the new session).\r\n useEffect(() => {\r\n if (activeIndex < 0) return\r\n const el = navRef.current\r\n if (el === null) return\r\n const item = el.querySelector<HTMLElement>(`[data-crl-index=\"${activeIndex}\"]`)\r\n if (item === null) return\r\n const target = item.offsetTop - el.clientHeight / 2 + item.clientHeight / 2\r\n el.scrollTo({ top: Math.max(0, target), behavior: 'smooth' })\r\n }, [activeIndex])\r\n\r\n // The rail hides on tiny sessions (fewer than 2 user messages) — but the\r\n // count is the UNFILTERED list: the favorites-only filter may legitimately\r\n // narrow to a single row, and hiding the rail then would strand the user\r\n // with no way to switch the filter back off.\r\n if (sessionId === undefined || messages.length < 2) return null\r\n\r\n // Fav-only filter remaps the tip index into the FULL list (tip content and\r\n // hover data always come from the unfiltered messages array).\r\n const tipIndex = tip === null\r\n ? -1\r\n : favOnly\r\n ? messages.findIndex((m) => effectiveMessages[tip.index] !== undefined\r\n && favoriteIdOfMessage(m) === favoriteIdOfMessage(effectiveMessages[tip.index]))\r\n : tip.index\r\n\r\n const items = effectiveMessages.map((m, i) => {\r\n const key = anchorKeyOf(m)\r\n const starred = favoriteIdOfMessage(m) !== '' && isFavorite(favorites, sessionId, favoriteIdOfMessage(m))\r\n return createElement('button', {\r\n type: 'button',\r\n key: m.seq,\r\n 'data-crl-index': String(i),\r\n className: S.item + (activeIndex === i ? ` ${S.itemActive}` : '') + (starred ? ` ${S.favItem}` : ''),\r\n 'aria-label': `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,\r\n 'aria-current': activeIndex === i ? 'location' : undefined,\r\n disabled: jumping,\r\n onClick: () => {\r\n if (key === undefined || jumping) return\r\n jumpAbortRef.current?.abort()\r\n const controller = new AbortController()\r\n jumpAbortRef.current = controller\r\n setJumping(true)\r\n void jumpToMessage(\r\n sessionsService as never,\r\n sessionId as string,\r\n key,\r\n (k) => chatNodeOf(fallbackStore.getSnapshot(), k),\r\n undefined,\r\n controller.signal,\r\n // DSH 0.1.2-alpha.3+ jump pager: the message's event seq lets the\r\n // rail page history with one exact loadThrough call instead of a\r\n // loadOlder loop (50 messages per page) from the window head.\r\n m.seq,\r\n ).finally(() => setJumping(false))\r\n },\r\n onMouseEnter: () => handleItemEnter(i),\r\n onMouseLeave: () => handleItemLeave(i),\r\n children: [\r\n createElement('span', { className: S.num }, `#${i + 1}`),\r\n starred\r\n ? createElement('span', { className: S.favStar, role: 'img', 'aria-label': t.fav, 'aria-hidden': undefined },\r\n createElement('svg', {\r\n viewBox: '0 0 24 24',\r\n width: 13,\r\n height: 13,\r\n fill: 'currentColor',\r\n stroke: 'none',\r\n 'aria-hidden': true,\r\n },\r\n createElement('path', { d: STAR_PATH, 'aria-hidden': true })))\r\n : null,\r\n createElement('span', { className: S.title }, m.text === '' ? t.noText : m.text),\r\n m.hasImage\r\n ? createElement('span', { className: S.img, role: 'img', 'aria-label': t.hasImage },\r\n createElement('svg', {\r\n viewBox: '0 0 16 16',\r\n width: 12,\r\n height: 12,\r\n fill: 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 1.4,\r\n strokeLinecap: 'round',\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n },\r\n createElement('rect', { x: 1.7, y: 2.7, width: 12.6, height: 10.6, rx: 2.2 }),\r\n createElement('circle', { cx: 5.6, cy: 6.4, r: 1.15, fill: 'currentColor', stroke: 'none' }),\r\n createElement('path', { d: 'M2.5 11.6l3.6-3.1 2.6 2.2 2.1-1.9 3.3 2.8' })))\r\n : null,\r\n createElement('span', { className: S.time }, relativeTime(m.time, t)),\r\n createElement('span', { className: S.ind, 'aria-hidden': true },\r\n createElement('span', { className: S.line })),\r\n ],\r\n })\r\n })\r\n\r\n return createPortal(\r\n // Fragment: the nav (transformed) and the tip must be siblings — a fixed\r\n // child inside a transformed ancestor is positioned relative to that\r\n // ancestor, breaking the viewport coordinates the tip uses.\r\n [createElement('div', {\r\n ref: navRef,\r\n className: S.nav + (show ? ` ${S.navShow}` : ''),\r\n role: 'navigation',\r\n 'aria-label': t.railLabel,\r\n onMouseEnter: () => setShow(true),\r\n onMouseLeave: () => setShow(false),\r\n children: [\r\n // Favorites-only filter: a star pill pinned at the top of the rail\r\n // (sticky; it never scrolls away with the rows). On state fills the\r\n // star (yellow) and narrows the rail to favorited messages; the label\r\n // (\"bookmarks only\") only shows once the rail expands — the collapsed\r\n // 36px capsule keeps a clean round icon. Hidden entirely when this\r\n // session has no favorites (nothing to filter to).\r\n showFavToggle\r\n ? createElement('button', {\r\n type: 'button',\r\n key: 'favToggle',\r\n className: S.favToggle + (favOnly ? ' crl_on' : ''),\r\n 'aria-pressed': favOnly,\r\n 'aria-label': t.favOnly,\r\n title: t.favOnly,\r\n onMouseEnter: (e: MouseEvent) => e.stopPropagation(),\r\n onClick: () => setFavOnly((v) => !v),\r\n children: [\r\n createElement('svg', {\r\n viewBox: '0 0 24 24',\r\n width: 14,\r\n height: 14,\r\n fill: favOnly ? 'currentColor' : 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 2,\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n }, createElement('path', { d: STAR_PATH })),\r\n createElement('span', { className: S.favToggleLabel }, t.favOnly),\r\n ],\r\n })\r\n : null,\r\n jumping ? createElement('div', { className: S.loading, key: 'loading' },\r\n createElement('span', { className: S.loadingLabel }, t.loading)) : null,\r\n ...items,\r\n ],\r\n }),\r\n // Full-content panel: anchored to the left of the hovered item. Images\r\n // stack above the text; a message whose image cannot be resolved (no\r\n // projection refs and unloaded node) shows a quiet \"has image\" badge.\r\n tip !== null && tipIndex >= 0 && tipIndex < messages.length\r\n ? createElement('div', {\r\n className: S.tip,\r\n style: { left: `${tip.x}px`, top: `${tip.y}px`, transform: 'translateX(-100%)' },\r\n }, (() => {\r\n const m = messages[tipIndex]\r\n const specs = tipImagesOf(m, nodeSnapshot)\r\n const children: ReactNode[] = []\r\n if (specs.length > 0 && readThumb !== undefined) {\r\n // Only the first image renders; the rest collapse into a \"+N\"\r\n // badge so a many-image message cannot push the tip past its\r\n // viewport cap (the stacked thumbnails did).\r\n children.push(createElement('div', { className: S.tipImgs, key: 'imgs' },\r\n createElement('div', { className: S.tipImgWrap, key: 'img' },\r\n createElement(TipThumb, { spec: specs[0], sessionId: sessionId as string, read: readThumb }),\r\n specs.length > 1\r\n ? createElement('span', { className: S.tipImgCount, 'aria-hidden': true }, `+${specs.length - 1}`)\r\n : null)))\r\n } else if (m.hasImage) {\r\n children.push(createElement('span', { className: S.tipBadge, key: 'badge' }, t.hasImage))\r\n }\r\n children.push(createElement('span', { key: 'text' }, fullTextOf(m, nodeSnapshot) || t.noText))\r\n return children\r\n })())\r\n : null],\r\n document.body,\r\n )\r\n}\r\n\r\nfunction apply(ctx: ClientContext): void {\r\n // Current DSH (0.1.2-alpha.2+) moved Conversation target data out of the\r\n // Session snapshot: chat nodes live in the uiConversation `'chat'` view\r\n // target. Resolved through optional ctx.inject (own fiber, never blocking\r\n // this plugin) so older DSH without the service keeps the legacy Session\r\n // snapshot fallback untouched.\r\n let chatOf: ((sessionId: string) => ChatViewSource | undefined) | undefined\r\n ctx.inject(['uiConversation'], (scope: ClientContext) => {\r\n const ui = (scope as unknown as { uiConversation: UiConversationRuntime }).uiConversation\r\n chatOf = (sessionId: string): ChatViewSource | undefined => {\r\n try {\r\n const binding = ui.binding(sessionId)\r\n return binding.target('chat') as unknown as ChatViewSource | undefined\r\n } catch {\r\n // Unknown session: the rail already gates on a live session id.\r\n return undefined\r\n }\r\n }\r\n })\r\n ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({\r\n name: 'conversation.input.dock',\r\n id: 'chat-rail',\r\n order: 40,\r\n inject: () => ({\r\n sessionsService: ctx.sessions,\r\n chatOf,\r\n // createDraftImages lives on the concrete ConversationController, not\r\n // the outward IConversation face; the runtime service is always the\r\n // controller, so the cast is structural, never a feature guess.\r\n conversation: ctx.conversation as unknown as { createDraftImages(files: readonly File[]): readonly { id: string }[] },\r\n }),\r\n }, TimelineRail))\r\n // 设置(设置——插件页):与 node-appearance 同构——注册「可配置插件」卡片(keyed by namespace)。\r\n ctx.inject(['settingsScope'], (scope) => {\r\n bindRailSettingsScope((scope as unknown as { settingsScope: unknown }).settingsScope)\r\n })\r\n ctx.slots.inject(('settings.plugin.item') as never, () => ctx.slots.register({\r\n name: 'settings.plugin.item',\r\n key: 'chat-rail',\r\n inject: () => ({\r\n setShowOfficialNavigator: (show: boolean) => railSettingsScope?.set('showOfficialNavigator', show),\r\n t: langStrings(),\r\n }),\r\n } as never, ChatRailSettingsRow))\r\n}\r\n\r\n/** 设置——插件页卡片:chat-rail「使用官方轮次导航条」开关(对比模式,即时生效、免重启)。 */\r\nexport interface ChatRailSettingsRowFace {\r\n setShowOfficialNavigator(show: boolean): void\r\n t: Record<string, string>\r\n}\r\n\r\nexport function ChatRailSettingsRow({ setShowOfficialNavigator, t }: ChatRailSettingsRowFace): ReactNode {\r\n const [open, setOpen] = useState(false)\r\n const snap = useSyncExternalStore(\r\n railSettingsScope !== null ? (cb: () => void) => railSettingsScope!.subscribe(cb) : NOOP_SUB,\r\n () => railSettingsScope?.getSnapshot() ?? { value: undefined },\r\n )\r\n const on = snap.value?.showOfficialNavigator === true\r\n return createElement('li', { className: 'crlSetCard' + (open ? ' crlSetCardOpen' : '') },\r\n createElement('button', {\r\n type: 'button',\r\n className: 'crlSetHeader',\r\n 'aria-expanded': open,\r\n onClick: () => setOpen((v) => !v),\r\n children: [\r\n createElement('span', { className: 'crlSetHeadText' },\r\n createElement('span', { className: 'crlSetName' }, t.cardName),\r\n createElement('span', { className: 'crlSetDesc' }, t.cardDesc),\r\n ),\r\n createElement('svg', {\r\n className: 'crlSetChevron' + (open ? ' crlSetChevronOn' : ''),\r\n viewBox: '0 0 14 14', width: 14, height: 14,\r\n fill: 'none', 'aria-hidden': true,\r\n }, createElement('path', {\r\n d: 'M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z',\r\n fill: 'currentColor',\r\n })),\r\n ],\r\n }),\r\n open ? createElement('div', { className: 'crlSetBody', children: [\r\n createElement('div', { className: 'crlSetRow' }, [\r\n createElement('div', { className: 'crlSetRowLabel' },\r\n createElement('span', { className: 'crlSetHint' }, t.officialNavHint),\r\n ),\r\n createElement('button', {\r\n type: 'button',\r\n role: 'switch',\r\n 'aria-checked': on,\r\n className: 'crlSetSwitch' + (on ? ' on' : ''),\r\n onClick: () => setShowOfficialNavigator(!on),\r\n children: createElement('span', { className: 'crlSetKnob' }),\r\n }),\r\n ]),\r\n ] }) : null,\r\n )\r\n}\r\n\r\nexport { apply, TimelineRail, imageSpecsOfContent, normalize, jumpToMessage }\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useState","createElement","useSyncExternalStore","useRef","useMemo","useCallback","createPortal"],"sources":["../src/client/index.tsx"],"sourcesContent":["/**\r\n * @max-null/dsh-chat-rail — web client half.\r\n *\r\n * Right-edge conversation navigation rail: one indicator per user message,\r\n * scroll-spy highlight of the reading position, hover preview, click to jump.\r\n *\r\n * Two deliberate fixes over the dsh-chat-timeline reference:\r\n *\r\n * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout\r\n * push through the `--dsh-sidebar-width` CSS variable on `:root` (the\r\n * `#root` margin-right transition reads the same variable). The rail's\r\n * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same\r\n * transition timing, so rail and scrollport move together while the panel\r\n * expands — no \"panel first, scrollbar later\" lag.\r\n *\r\n * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)\r\n * lines — nearly invisible on a light background. Here the rail keeps a\r\n * constant translucent capsule, thicker/darker indicator lines, and an\r\n * always-visible collapsed state (no \"reveal on pointer proximity\").\r\n *\r\n * Data: host `chatRail` projection first, loaded chat nodes fallback, then a\r\n * background loadOlder loop (stopped as soon as the projection delivers).\r\n * Mounted in conversation.input.dock, portal-rendered to body.\r\n */\r\n\r\nimport { createElement, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\r\nimport type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'\r\n// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry).\r\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\r\n\r\nexport const inject = ['slots', 'sessions', 'conversation', 'settingsScope']\r\n\r\n// ---- i18n (DSH locale-aware, zh/en) ----\r\ntype LocaleId = 'zh' | 'en'\r\nconst STRINGS: Record<LocaleId, Record<string, string>> = {\r\n zh: {\r\n railLabel: '消息导航',\r\n roleUser: '用户',\r\n noText: '(无文本内容)',\r\n ariaJump: '跳转到消息',\r\n hasImage: '含图片',\r\n loading: '加载中…',\r\n timeJustNow: '刚刚',\r\n timeMinutes: '{n}分钟前',\r\n timeHours: '{n}小时前',\r\n timeDays: '{n}天前',\r\n fav: '收藏消息',\r\n unfav: '取消收藏',\r\n fill: '填充到输入框',\r\n filled: '已填入输入框',\r\n favOnly: '只显示收藏',\r\n settings: '设置',\r\n cardName: '消息导航条',\r\n cardDesc: '画卷式消息导航栏(chat-rail)——每条用户消息一个指示点,scroll-spy 跟随',\r\n officialNav: '使用官方轮次导航条(对比模式)',\r\n officialNavHint: '开启后隐藏本导航条、显示官方 TurnNavigator——便于与官方轮次导航对比(免重启即时生效)',\r\n },\r\n en: {\r\n railLabel: 'Message rail',\r\n roleUser: 'User',\r\n noText: '(no text)',\r\n ariaJump: 'Jump to message',\r\n hasImage: 'Has image',\r\n loading: 'Loading…',\r\n timeJustNow: 'just now',\r\n timeMinutes: '{n}m ago',\r\n timeHours: '{n}h ago',\r\n timeDays: '{n}d ago',\r\n fav: 'Bookmark message',\r\n unfav: 'Remove bookmark',\r\n fill: 'Fill into input',\r\n filled: 'Filled into input',\r\n favOnly: 'Bookmarks only',\r\n settings: 'Settings',\r\n cardName: 'Message rail',\r\n cardDesc: 'Canvas-style message navigation (chat-rail) — one dot per user message, scroll-spy follow',\r\n officialNav: 'Use the official turn navigation (comparison mode)',\r\n officialNavHint: 'Hides this rail and shows the official TurnNavigator — for A/B comparison with the official turn rail (applies instantly, no restart).',\r\n },\r\n}\r\n\r\n/** Compact relative time for a message timestamp (zh/en via template). */\r\nfunction relativeTime(ts: number, s: Record<string, string>): string {\r\n const diff = Date.now() - ts\r\n if (diff < 60_000) return s.timeJustNow\r\n if (diff < 3_600_000) return s.timeMinutes.replace('{n}', String(Math.floor(diff / 60_000)))\r\n if (diff < 86_400_000) return s.timeHours.replace('{n}', String(Math.floor(diff / 3_600_000)))\r\n if (diff < 7 * 86_400_000) return s.timeDays.replace('{n}', String(Math.floor(diff / 86_400_000)))\r\n const d = new Date(ts)\r\n return `${d.getMonth() + 1}/${d.getDate()}`\r\n}\r\n\r\n// ---- styles ----\r\n// 画卷式导航:单个容器,折叠态 36px 竖条;hover 时宽度从右往左展开到 280px\r\n// (打开画卷效果)。容器垂直方向在「去掉底栏后的可用区域」内居中:\r\n// top = (100vh - 底栏高)/2 + translateY(-50%),底栏收起时正中、展开时上移避让。\r\n// right 跟随 --dsh-sidebar-width、top 跟随 --dsh-sidebar-height,与 better-sidebar\r\n// 面板共享同一 CSS 变量 + transition,动画同步。\r\nconst css = [\r\n '.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:none;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}',\r\n '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)}',\r\n '.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)}',\r\n '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)}',\r\n // Scrollbar hidden by design: the capsule is small and the marks read as a\r\n // free-floating sequence — a bar that appears and disappears with the busy\r\n // row (and with overflow peaks) flickers worse than no bar at all. Scrolling\r\n // still works (overflow-y stays auto when expanded).\r\n '.crl_nav::-webkit-scrollbar{width:0;height:0}',\r\n // Jump-in-progress indicator: sticky row pinned at the rail top. The spinner\r\n // icon is always visible; the \"Loading…\" label only appears once expanded\r\n // (in the collapsed 36px rail the text would overflow the capsule).\r\n '.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}',\r\n '.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}',\r\n '.crl_loading .crl_loadingLabel{display:none}',\r\n '.crl_show .crl_loading .crl_loadingLabel{display:inline}',\r\n '@keyframes crl-spin{to{transform:rotate(360deg)}}',\r\n // Item row: always flex-end (the indicator rides the right edge through the\r\n // width animation, never jumping). The collapsed padding (0 9px) centers the\r\n // 18px indicator in the 36px capsule; the expanded padding (0 14px) clears\r\n // the text. Padding transitions with the same timing as the width, so the\r\n // indicator glides smoothly instead of snapping when text hides/shows.\r\n '.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}',\r\n '.crl_show .crl_item{padding:0 14px}',\r\n '.crl_item:hover{color:rgba(0,0,0,.95)}',\r\n '.crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_item,[data-theme=\\'dark\\'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}',\r\n 'body[data-ds-dark-theme] .crl_item:hover,[data-theme=\\'dark\\'] .crl_item:hover,.dark .crl_item:hover{color:rgba(255,255,255,.95)}',\r\n // Title / ordinal / time: display:none in collapsed state (so they take no\r\n // flex space and the indicator line stays centered), revealed on expand.\r\n // The ordinal has a fixed width (right-aligned) so message text starts at\r\n // the same x across items; time sits AFTER the title (no fixed width, so it\r\n // never shrinks the text area).\r\n '.crl_title{display:none;font-size:13px;line-height:20px;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;flex:1;min-width:0;text-align:left;overflow:hidden;color:inherit}',\r\n '.crl_show .crl_title{display:block;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_active .crl_title{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:500}',\r\n '.crl_num{display:none;flex-shrink:0;width:28px;font-size:10px;line-height:20px;color:rgba(0,0,0,.35);margin-right:8px;text-align:right;user-select:none}',\r\n '.crl_show .crl_num{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_num,[data-theme=\\'dark\\'] .crl_num,.dark .crl_num{color:rgba(255,255,255,.35)}',\r\n '.crl_time{display:none;flex-shrink:0;font-size:10px;line-height:20px;color:rgba(0,0,0,.28);margin-right:10px;user-select:none;white-space:nowrap}',\r\n '.crl_show .crl_time{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_time,[data-theme=\\'dark\\'] .crl_time,.dark .crl_time{color:rgba(255,255,255,.28)}',\r\n '@keyframes crl-fade{from{opacity:0}to{opacity:1}}',\r\n // Indicator line: 10×3px, clearly visible in both themes.\r\n '.crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}',\r\n '.crl_show .crl_ind{margin-left:8px}',\r\n // Image badge: hidden in the collapsed state (the rail is a clean 36px\r\n // capsule of indicator lines; an icon there would crowd and distract) and\r\n // revealed with the other labels on expand. A quiet stroke glyph in a muted\r\n // tone that ignores the active-blue text color.\r\n '.crl_img{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-left:6px;color:rgba(0,0,0,.38)}',\r\n '.crl_show .crl_img{display:inline-flex;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_img,[data-theme=\\'dark\\'] .crl_img,.dark .crl_img{color:rgba(255,255,255,.38)}',\r\n '.crl_line{background-color:rgba(0,0,0,.45);border-radius:4px;flex-shrink:0;width:10px;height:3px;transition:background-color .2s ease,transform .2s ease}',\r\n '.crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}',\r\n '.crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe);transform-origin:50%;transform:scale(1.4);box-shadow:0 0 6px var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_line,[data-theme=\\'dark\\'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}',\r\n '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)}',\r\n '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)}',\r\n // Full-content hover panel: floats to the LEFT of the expanded rail list.\r\n '.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}',\r\n '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)))}',\r\n // Tip thumbnail gallery: stacked above the text, one row per image, capped\r\n // height so a tall screenshot cannot swallow the preview panel. The\r\n // placeholder pulses while session.readAttachment resolves the bytes; a\r\n // failed load removes the row entirely rather than showing a broken image.\r\n '.crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}',\r\n '.crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}',\r\n '.crl_tipImg{display:block;max-width:100%;max-height:150px;width:auto;height:auto;object-fit:contain;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)))}',\r\n '.crl_tipImgCount{position:absolute;right:6px;bottom:6px;padding:0 6px;font-size:10px;line-height:16px;border-radius:9px;background:rgba(0,0,0,.55);color:rgba(255,255,255,.95);pointer-events:none}',\r\n '.crl_tipImgPh{flex-shrink:0;height:56px;border-radius:8px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.05)));animation:crl-pulse 1.2s ease-in-out infinite}',\r\n 'body[data-ds-dark-theme] .crl_tipImg,[data-theme=\\'dark\\'] .crl_tipImg,.dark .crl_tipImg{border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}',\r\n '.crl_tipBadge{display:inline-block;margin-bottom:8px;padding:0 7px;font-size:10px;line-height:16px;border-radius:9px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.06)));color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.45)))}',\r\n '@keyframes crl-pulse{0%,100%{opacity:.45}50%{opacity:.85}}',\r\n // Message action buttons injected next to the copy button (DOM row: the\r\n // user-message IconActions row). The host rows use 28px round icon-action\r\n // buttons (padding 6, 16px glyphs, DSW token colors, hover fill) — the\r\n // injected buttons mirror the exact geometry and token surface so the star\r\n // and plus read as native siblings of copy/branch. The star lights\r\n // yellowish (#ffd166, milestone-compatible) once favorited.\r\n // Tooltip: same dark pill as the host Tooltip (black rounded label, no\r\n // native title balloon). The delay mirrors the host tooltip's settle; the\r\n // fade is short so the pill appears right beside the hovered button.\r\n '.crl_msgAct{position:relative;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:0;padding:6px;border:none;border-radius:28px;background:transparent;color:var(--dsw-alias-label-tertiary,rgba(0,0,0,.42));cursor:pointer;transition:background .15s ease,color .15s ease}',\r\n '.crl_msgAct:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06));color:var(--dsw-alias-label-secondary,rgba(0,0,0,.72))}',\r\n '.crl_msgAct:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4d6bfe);outline-offset:2px}',\r\n '.crl_msgAct[data-tip]:hover::after{content:attr(data-tip);position:absolute;left:50%;bottom:calc(100% + 8px);transform:translateX(-50%);padding:5px 10px;border-radius:8px;background:rgba(24,28,36,.94);color:rgba(255,255,255,.95);font-size:12px;line-height:1.4;white-space:nowrap;pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}',\r\n '.crl_msgAct[data-tip]:hover::before{content:\"\";position:absolute;left:50%;bottom:calc(100% + 3px);transform:translateX(-50%);border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid rgba(24,28,36,.94);pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}',\r\n '@keyframes crl-tip-in{to{opacity:1}}',\r\n '.crl_msgAct.crl_fav.crl_on{color:#ffd166}',\r\n '.crl_msgAct.crl_fav.crl_on svg{fill:currentColor}',\r\n '.crl_msgAct.crl_fav.crl_on:hover{color:#ffd166}',\r\n // Rail row favorite badge + yellow indicator line for favorited messages.\r\n '.crl_favStar{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-right:6px;color:#ffd166}',\r\n '.crl_show .crl_favStar{display:inline-flex;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_favItem .crl_line{background-color:#ffd166}',\r\n '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}',\r\n // A favorited line that is also the current reading position keeps its\r\n // yellow identity — the generic active glow is blue, which pairs miserably\r\n // with the gold line, so the favorite-active state recasts the glow in the\r\n // same warm tone.\r\n '.crl_item.crl_favItem.crl_active .crl_line{background-color:#ffd166;box-shadow:0 0 6px #ffd166}',\r\n // Rail-top favorites-only toggle: a round icon pill in the collapsed state;\r\n // on expand it becomes a full-width label row (star + \"bookmarks only\") so\r\n // the control reads as a real rail header action instead of a stray dot.\r\n // STICKY: the expanded rail's rows scroll inside the capsule (overflow-y),\r\n // and the pill must stay pinned at the top instead of scrolling away with\r\n // them. The capsule has padding:10px 0 — sticky `top:0` would leave the\r\n // last 10px of the scrollport uncovered, letting a passing row peek through\r\n // above the pill. top:-10px shifts the pin edge to the capsule's true top\r\n // (padding included), so rows scroll fully underneath.\r\n '.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}',\r\n '.crl_favToggle:hover{background:rgba(0,0,0,.07);color:rgba(0,0,0,.75)}',\r\n '.crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.14)}',\r\n '.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}',\r\n '.crl_favToggleLabel{display:none}',\r\n '.crl_show .crl_favToggleLabel{display:inline-block;min-width:0;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}',\r\n 'body[data-ds-dark-theme] .crl_favToggle,[data-theme=\\'dark\\'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4)}',\r\n '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)}',\r\n '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)}',\r\n '@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}',\r\n '.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}',\r\n '.crlSetCard:hover{border-color:var(--dsw-alias-label-dimmed)}',\r\n '.crlSetCardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}',\r\n '.crlSetHeader{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px}',\r\n '.crlSetHeadText{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}',\r\n '.crlSetName{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}',\r\n '.crlSetDesc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}',\r\n '.crlSetChevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s}',\r\n '.crlSetChevronOn{transform:rotate(180deg)}',\r\n '.crlSetBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}',\r\n '.crlSetRow{position:relative;display:flex;align-items:center;gap:10px;padding:10px 0 4px;cursor:default}',\r\n '.crlSetRowLabel{flex:1;min-width:0;font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}',\r\n '.crlSetHint{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary);font-weight:400}',\r\n '.crlSetSwitch{flex:none;width:40px;height:22px;border:none;border-radius:11px;cursor:pointer;padding:0;background:var(--dsw-alias-border-l4,rgba(0,0,0,.16));transition:background .15s}',\r\n '.crlSetSwitch.on{background:var(--dsw-alias-state-business-primary,#4FC3F7)}',\r\n '.crlSetKnob{display:block;width:16px;height:16px;border-radius:8px;background:#fff;margin-left:2px;transition:margin-left .15s;pointer-events:none}',\r\n '.crlSetSwitch.on .crlSetKnob{margin-left:22px}',\r\n].join('')\r\n\r\n/** 屏蔽官方 TurnNavigator(轮次导航竖轨,DSH 0.1.2-alpha.1+)的独立样式(不再无条件注入——由\r\n * 「使用官方轮次导航条」设置开关控制):官方 rail 与本插件 rail 同处会话面板右缘且只支持\r\n * 「轮次」维度,默认隐藏官方、由本插件统一承担消息导航。锚点用 aria-label(zh/en 双文案)而非\r\n * CSS module 类名(如 hkplfa_slot,hash 随构建内容漂移)。 */\r\nconst HIDE_OFFICIAL_CSS = 'nav[aria-label=\"轮次导航\"],nav[aria-label=\"Turn navigation\"]{display:none !important}'\r\nconst HIDE_OFFICIAL_STYLE_ID = 'data-crl-hide-official'\r\n\r\n/** 按设置同步「屏蔽官方 TurnNavigator」样式(showOfficial=true=官方模式:不屏蔽)。 */\r\nexport function syncOfficialHide(showOfficial: boolean): void {\r\n const el = document.head.querySelector<HTMLStyleElement>(`style[${HIDE_OFFICIAL_STYLE_ID}]`)\r\n if (showOfficial) {\r\n if (el !== null) el.remove()\r\n return\r\n }\r\n if (el === null) {\r\n const tag = document.createElement('style')\r\n tag.setAttribute(HIDE_OFFICIAL_STYLE_ID, '')\r\n tag.textContent = HIDE_OFFICIAL_CSS\r\n document.head.appendChild(tag)\r\n }\r\n}\r\n\r\n/** 移除所有已注入的行内收藏/填充按钮(官方模式下的回溯清理)。 */\r\nexport function clearRowActions(): void {\r\n for (const row of document.querySelectorAll<HTMLElement>('[data-crl-actions=\"1\"]')) {\r\n for (const btn of row.querySelectorAll<HTMLElement>('[data-crl-star],[data-crl-fill]')) btn.remove()\r\n row.dataset.crlActions = ''\r\n }\r\n}\r\n\r\n// 「使用官方轮次导航条」设置(2026-09-05 对比模式):与 node-appearance 同构——官方\r\n// settingsScope(设置——插件页的「可配置插件」卡片,keyed by namespace 'chat-rail')。\r\ntype RailSettings = { showOfficialNavigator?: boolean }\r\nlet railSettingsScope: (({\r\n getSnapshot(): { value?: RailSettings }\r\n subscribe(listener: () => void): () => void\r\n}) & {\r\n set(key: 'showOfficialNavigator', value: boolean): Promise<unknown>\r\n}) | null = null\r\n\r\n/** client apply 时绑定(settingsScope 官方服务提供)。 */\r\nexport function bindRailSettingsScope(scope: unknown): void {\r\n if (typeof scope !== 'object' || scope === null) return\r\n railSettingsScope = (scope as {\r\n bind<T>(spec: { namespace: string }): {\r\n getSnapshot(): { value?: T }\r\n subscribe(listener: () => void): () => void\r\n set(key: keyof T & string, value: T[keyof T & string]): Promise<unknown>\r\n }\r\n }).bind<RailSettings>({ namespace: 'chat-rail' }) as never\r\n}\r\n\r\n/** No-op subscribe for useSyncExternalStore when the scope is not yet bound. */\r\nconst NOOP_SUB = (() => () => {}) as () => () => void\r\n\r\nconst STYLE_ID = '@max-null/dsh-chat-rail/styles.module.css'\r\nif (typeof document !== 'undefined' && document.querySelector(`style[data-plugin-css=\"${STYLE_ID}\"]`) === null) {\r\n const tag = document.createElement('style')\r\n tag.dataset.plugin = 'dsh-chat-rail'\r\n tag.dataset.pluginCss = STYLE_ID\r\n tag.textContent = css\r\n document.head.appendChild(tag)\r\n}\r\n\r\nconst S = {\r\n nav: 'crl_nav',\r\n navShow: 'crl_show',\r\n item: 'crl_item',\r\n itemActive: 'crl_active',\r\n title: 'crl_title',\r\n num: 'crl_num',\r\n time: 'crl_time',\r\n ind: 'crl_ind',\r\n line: 'crl_line',\r\n img: 'crl_img',\r\n loading: 'crl_loading',\r\n loadingLabel: 'crl_loadingLabel',\r\n tip: 'crl_tip',\r\n tipImgs: 'crl_tipImgs',\r\n tipImgWrap: 'crl_tipImgWrap',\r\n tipImg: 'crl_tipImg',\r\n tipImgCount: 'crl_tipImgCount',\r\n tipImgPh: 'crl_tipImgPh',\r\n tipBadge: 'crl_tipBadge',\r\n favStar: 'crl_favStar',\r\n favItem: 'crl_favItem',\r\n favToggle: 'crl_favToggle',\r\n favToggleLabel: 'crl_favToggleLabel',\r\n}\r\n\r\n// ---- data helpers ----\r\nconst NOOP_STORE = { getSnapshot: () => undefined, subscribe: () => () => {} }\r\n\r\n// ---- favorites (persisted per-session bookmark store) ----\r\n// Shape: Record<sessionId, messageId[]>. The durable message id is the same\r\n// identity the host projection emits (`id`) and the DOM anchor encodes\r\n// (`13:input-message<id>`), so DOM-injected buttons and the rail agree on\r\n// the key with no projection hop.\r\n// 持久化(2026-08-27):host 文件(~/.dsh/chat-rail-favorites.json,经\r\n// /chat-rail/api/favorites GET/PUT)——localStorage 按 origin 隔离,思灵 DSH\r\n// web 端口每次启动随机 → 跨重启丢弃。localStorage 仅作旧数据一次性迁移。\r\nconst FAVORITES_KEY = '@max-null/dsh-chat-rail:favorites'\r\ntype FavoritesMap = Record<string, string[]>\r\n\r\nconst FAVORITES_API = '/chat-rail/api/favorites'\r\n/** 模块级快照缓存(host 加载后充当源 truth)+ 订阅者集合。 */\r\nlet favoritesCache: FavoritesMap | null = null\r\nconst favoritesListeners = new Set<() => void>()\r\n\r\n/** 从 localStorage 读旧数据(迁移用/离线兜底)。 */\r\nfunction readFavoritesLocal(): FavoritesMap {\r\n if (typeof localStorage === 'undefined') return {}\r\n try {\r\n const raw = localStorage.getItem(FAVORITES_KEY)\r\n if (raw === null) return {}\r\n const value: unknown = JSON.parse(raw)\r\n return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as FavoritesMap : {}\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\nasync function fetchFavoritesMap(): Promise<FavoritesMap> {\r\n try {\r\n const res = await fetch(FAVORITES_API, { headers: { Accept: 'application/json' } })\r\n if (!res.ok) return {}\r\n const data = await res.json() as { ok?: boolean, value?: unknown }\r\n const value = data?.value\r\n return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as FavoritesMap : {}\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\n/** 模块加载时同步 host;旧 localStorage 数据迁移一次(host 为空且本地有值)。 */\r\nvoid (async () => {\r\n const host = await fetchFavoritesMap()\r\n if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {\r\n const legacy = readFavoritesLocal()\r\n if (Object.keys(host).length === 0 && Object.keys(legacy).length > 0) {\r\n await fetch(FAVORITES_API, {\r\n method: 'PUT', headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ favorites: legacy }),\r\n }).catch(() => {})\r\n }\r\n try { localStorage.removeItem(FAVORITES_KEY) } catch { /* 忽略 */ }\r\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\r\n favoritesCache = Object.keys(host).length === 0 ? legacy : host\r\n // host 就位后通知快照订阅(rail/收藏按钮重渲染)\r\n for (const listener of favoritesListeners) listener()\r\n } else {\r\n favoritesCache = host\r\n }\r\n})()\r\n\r\n/** Read the favorites map(内存镜像;未加载完成时回退 localStorage)。 */\r\nexport function readFavorites(): FavoritesMap {\r\n if (favoritesCache !== null) return favoritesCache\r\n return readFavoritesLocal()\r\n}\r\n\r\n/** Write the favorites map;更新内存并 fire-and-forget 持久化到 host(host-only)。 */\r\nfunction writeFavorites(map: FavoritesMap): void {\r\n favoritesCache = map\r\n void fetch(FAVORITES_API, {\r\n method: 'PUT', headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ favorites: map }),\r\n }).catch(() => { /* 网络失败:内存仍在用(下次写会重试) */ })\r\n}\r\n\r\n/** Normalized id list for one session (drops non-string entries). */\r\nexport function favoriteIdsOf(map: FavoritesMap, sessionId: string): string[] {\r\n const list = map[sessionId]\r\n return Array.isArray(list) ? list.filter((id): id is string => typeof id === 'string') : []\r\n}\r\n\r\n/** Toggle one id in a list (stable new array; no in-place mutation). */\r\nexport function toggleFavoriteId(list: string[], id: string): string[] {\r\n return list.includes(id) ? list.filter((candidate) => candidate !== id) : [...list, id]\r\n}\r\n\r\n/** Whether one message is favorited in one session. */\r\nexport function isFavorite(map: FavoritesMap, sessionId: string, messageId: string): boolean {\r\n return favoriteIdsOf(map, sessionId).includes(messageId)\r\n}\r\n\r\n/** Stable snapshot for useSyncExternalStore (loaded once, cached until toggle). */\r\nfunction favoritesSnapshot(): FavoritesMap {\r\n if (favoritesCache === null) favoritesCache = readFavorites()\r\n return favoritesCache\r\n}\r\n\r\n/** Subscribe to favorites changes; returns the unsubscribe. */\r\nfunction subscribeFavorites(listener: () => void): () => void {\r\n favoritesListeners.add(listener)\r\n return () => { favoritesListeners.delete(listener) }\r\n}\r\n\r\n/** Toggle one message's favorite state: snapshot → new map → persist → notify. */\r\nfunction toggleFavorite(sessionId: string, messageId: string): void {\r\n const next = { ...favoritesSnapshot() }\r\n next[sessionId] = toggleFavoriteId(favoriteIdsOf(favoritesSnapshot(), sessionId), messageId)\r\n favoritesCache = next\r\n writeFavorites(next)\r\n for (const listener of [...favoritesListeners]) listener()\r\n}\r\n\r\n/** Remember the messageId read off a DOM anchor key (`13:input-message<id>`). */\r\nexport function messageIdOfAnchorKey(key: string): string {\r\n return key.startsWith('13:input-message') ? key.slice('13:input-message'.length) : key\r\n}\r\n\r\n/** The favorite key of one rail message: the durable id when present (host\r\n * projection), otherwise the id reconstructed from the node anchor (the\r\n * loaded-node fallback path). */\r\nfunction favoriteIdOfMessage(m: Pick<RailMessage, 'id' | 'key'>): string {\r\n if (typeof m.id === 'string' && m.id !== '') return m.id\r\n if (typeof m.key === 'string') return messageIdOfAnchorKey(m.key)\r\n return ''\r\n}\r\n\r\n// ---- message-action injector (DOM) ----\r\n// User-message rows have no official action slot (assistant-actions serves\r\n// assistant turns only), so the star (favorite) and plus (fill-to-input)\r\n// buttons are injected next to the copy button of each user row. The rows\r\n// are located by their anchor key so history paging and re-renders both\r\n// land; the injector is a MutationObserver that re-scans after any DOM\r\n// change and never double-injects (row marker).\r\n//\r\n// Runtime wiring lives in a module-level context the rail component updates\r\n// every render (session id + handlers), so the pure DOM buttons stay tiny:\r\n// they only read data attributes and call context handlers.\r\nconst actionCtx: {\r\n sessionId: string | undefined\r\n onToggleFavorite: (messageId: string) => void\r\n onFill: (messageId: string) => void\r\n} = {\r\n sessionId: undefined,\r\n onToggleFavorite: () => {},\r\n onFill: () => {},\r\n}\r\n\r\n/** aria-label values of the copy button across shipped locales (button sits\r\n * in the user row's IconActions; its parent is the row's action host). */\r\nconst COPY_ARIA_LABELS = new Set(['复制', 'Copy', '已复制', 'Copied'])\r\n\r\n/** The copy button inside one user message row (null until rendered). */\r\nfunction copyButtonOf(row: HTMLElement): HTMLButtonElement | null {\r\n for (const button of row.querySelectorAll<HTMLButtonElement>('button')) {\r\n const label = button.getAttribute('aria-label')\r\n if (label !== null && COPY_ARIA_LABELS.has(label)) return button\r\n }\r\n return null\r\n}\r\n\r\n/** Star SVG path (milestone-compatible glyph, 24-unit viewBox). */\r\nconst STAR_PATH = 'm12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'\r\n\r\n/** Build the favorite (star) button for one user message row. */\r\nfunction favoriteButtonOf(messageId: string, lang: Record<string, string>): HTMLButtonElement {\r\n const button = document.createElement('button')\r\n button.type = 'button'\r\n button.className = 'crl_msgAct crl_fav'\r\n button.dataset.crlStar = messageId\r\n const initialLabel = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? lang.unfav : lang.fav\r\n button.setAttribute('aria-pressed', String(isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId)))\r\n button.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? 'true' : undefined\r\n button.classList.toggle('crl_on', isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId))\r\n // The CSS tooltip reads data-tip (a short dark pill like the host Tooltip);\r\n // the native title balloon is intentionally absent — double tips would\r\n // stack a browser bubble over the styled one.\r\n button.setAttribute('data-tip', initialLabel)\r\n button.setAttribute('aria-label', initialLabel)\r\n button.innerHTML = `<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"${STAR_PATH}\"/></svg>`\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault()\r\n event.stopPropagation()\r\n // Read the id off the dataset at click time: the injector may refresh the\r\n // button for a new session and the closure must stay current.\r\n const id = button.dataset.crlStar\r\n if (id !== undefined) {\r\n actionCtx.onToggleFavorite(id)\r\n refreshFavoriteButton(button, lang)\r\n }\r\n })\r\n return button\r\n}\r\n\r\n/** Build the fill-to-input (plus) button for one user message row. */\r\nfunction fillButtonOf(messageId: string, lang: Record<string, string>): HTMLButtonElement {\r\n const button = document.createElement('button')\r\n button.type = 'button'\r\n button.className = 'crl_msgAct crl_fill'\r\n button.dataset.crlFill = messageId\r\n button.setAttribute('data-tip', lang.fill)\r\n button.setAttribute('aria-label', lang.fill)\r\n button.innerHTML = '<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" aria-hidden=\"true\"><path d=\"M8 3.5v9M3.5 8h9\"/></svg>'\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault()\r\n event.stopPropagation()\r\n const id = button.dataset.crlFill\r\n if (id !== undefined) actionCtx.onFill(id)\r\n })\r\n return button\r\n}\r\n\r\n/** Sync one star button to its message's current favorite state. */\r\nfunction refreshFavoriteButton(button: HTMLButtonElement, lang: Record<string, string>): void {\r\n const id = button.dataset.crlStar\r\n if (id === undefined) return\r\n const starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', id)\r\n button.setAttribute('aria-pressed', String(starred))\r\n button.dataset.starred = starred ? 'true' : undefined\r\n button.classList.toggle('crl_on', starred)\r\n const label = starred ? lang.unfav : lang.fav\r\n // Both the a11y label and the styled tooltip follow the new state.\r\n button.setAttribute('data-tip', label)\r\n button.setAttribute('aria-label', label)\r\n}\r\n\r\n/** Refresh every injected star button (session switch / external toggle). */\r\nfunction refreshAllFavoriteButtons(lang: Record<string, string>): void {\r\n for (const button of document.querySelectorAll<HTMLButtonElement>('[data-crl-star]')) {\r\n refreshFavoriteButton(button, lang)\r\n }\r\n}\r\n\r\n/** Inject star + plus buttons into one user message row (idempotent). */\r\nfunction injectMessageActions(row: HTMLElement, lang: Record<string, string>): void {\r\n // Already injected and both buttons still present → leave it alone.\r\n if (row.dataset.crlActions === '1') {\r\n if (row.querySelector('[data-crl-star]') !== null && row.querySelector('[data-crl-fill]') !== null) return\r\n // React re-rendered the row and replaced the icons: fall through to re-inject.\r\n }\r\n const copy = copyButtonOf(row)\r\n const messageId = messageIdOfAnchorKey(row.getAttribute('data-chat-anchor-key') ?? '')\r\n if (copy === null || messageId === '') return\r\n const star = favoriteButtonOf(messageId, lang)\r\n star.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? 'true' : undefined\r\n star.classList.toggle('crl_on', isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId))\r\n const fill = fillButtonOf(messageId, lang)\r\n copy.before(star, fill)\r\n row.dataset.crlActions = '1'\r\n}\r\n\r\n/** Start observing new user rows so injected actions follow history paging. */\r\nfunction startActionInjector(lang: Record<string, string>): () => void {\r\n const scan = () => {\r\n for (const row of document.querySelectorAll<HTMLElement>('[data-chat-anchor-key^=\"13:input-message\"]')) {\r\n injectMessageActions(row, lang)\r\n }\r\n }\r\n const observer = new MutationObserver(scan)\r\n if (typeof document !== 'undefined' && document.body !== null) {\r\n observer.observe(document.body, { childList: true, subtree: true })\r\n }\r\n scan()\r\n return () => observer.disconnect()\r\n}\r\n\r\ninterface RailImage {\r\n attachmentId: string\r\n mediaType: string\r\n width: number\r\n height: number\r\n}\r\n\r\ninterface RailMessage {\r\n seq: number\r\n time: number\r\n text: string\r\n hasImage: boolean\r\n /** Stored-image references from the host projection (empty when inline-only). */\r\n images?: RailImage[]\r\n key?: string\r\n id?: string\r\n}\r\n\r\n/** One displayable tip image: either a durable reference (resolved lazily) or\r\n * an inline base64 payload already usable as an <img> src. */\r\ntype ImageSpec =\r\n | { kind: 'ref'; attachmentId: string; mediaType: string }\r\n | { kind: 'data'; src: string }\r\n\r\n/** Extract stored-image references from a ContentBlock list (reference form). */\r\nfunction nodeImagesOf(content: unknown): RailImage[] {\r\n if (!Array.isArray(content)) return []\r\n const out: RailImage[] = []\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a === null || typeof a !== 'object') continue\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown; width?: unknown; height?: unknown }\r\n if (typeof ref.attachmentId !== 'string' || ref.attachmentId === '') continue\r\n out.push({\r\n attachmentId: ref.attachmentId,\r\n mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png',\r\n width: typeof ref.width === 'number' ? ref.width : 0,\r\n height: typeof ref.height === 'number' ? ref.height : 0,\r\n })\r\n }\r\n return out\r\n}\r\n\r\n/** Convert a ContentBlock list to displayable image specs; inline base64\r\n * blocks (rare in replayed history) become data URLs on the spot. */\r\nfunction imageSpecsOfContent(content: unknown): ImageSpec[] {\r\n const specs: ImageSpec[] = []\r\n if (!Array.isArray(content)) return specs\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown; data?: unknown; mediaType?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a !== null && typeof a === 'object') {\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown }\r\n if (typeof ref.attachmentId === 'string' && ref.attachmentId !== '') {\r\n specs.push({ kind: 'ref', attachmentId: ref.attachmentId, mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png' })\r\n continue\r\n }\r\n }\r\n if (typeof b.data === 'string' && b.data !== '') {\r\n specs.push({ kind: 'data', src: `data:${typeof b.mediaType === 'string' ? b.mediaType : 'image/png'};base64,${b.data}` })\r\n }\r\n }\r\n return specs\r\n}\r\n\r\n/** Normalize one projection entry to a rail message. */\r\nfunction normalize(m: unknown): RailMessage | null {\r\n if (m === null || typeof m !== 'object') return null\r\n const o = m as Record<string, unknown>\r\n if (typeof o.seq !== 'number') return null\r\n // Host projection emits `text`; keep `preview` as a defensive fallback in\r\n // case an older projection payload is still cached in the browser.\r\n const text = typeof o.text === 'string' ? o.text : typeof o.preview === 'string' ? o.preview : ''\r\n const images = Array.isArray(o.images)\r\n ? o.images.map(nodeImagesOfEntry).filter((i): i is RailImage => i !== null)\r\n : undefined\r\n return {\r\n seq: o.seq,\r\n time: typeof o.time === 'number' ? o.time : 0,\r\n text,\r\n // Older projections lack hasImage; defensive default false.\r\n hasImage: o.hasImage === true,\r\n ...(images !== undefined && images.length > 0 ? { images } : {}),\r\n ...(typeof o.key === 'string' ? { key: o.key } : {}),\r\n ...(typeof o.id === 'string' ? { id: o.id } : {}),\r\n }\r\n}\r\n\r\n/** Normalize one projection entry of the `images` array (wire form). */\r\nfunction nodeImagesOfEntry(v: unknown): RailImage | null {\r\n if (v === null || typeof v !== 'object') return null\r\n const o = v as Record<string, unknown>\r\n if (typeof o.attachmentId !== 'string' || o.attachmentId === '') return null\r\n return {\r\n attachmentId: o.attachmentId,\r\n mediaType: typeof o.mediaType === 'string' ? o.mediaType : 'image/png',\r\n width: typeof o.width === 'number' ? o.width : 0,\r\n height: typeof o.height === 'number' ? o.height : 0,\r\n }\r\n}\r\n\r\n/** Extract preview text from a user message's ContentBlock list. */\r\nfunction userTextOf(content: unknown): string {\r\n if (!Array.isArray(content)) return ''\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n return out.trim().slice(0, 80)\r\n}\r\n\r\n/** Whether a ContentBlock list carries an image block (rc.8 attachments). */\r\nfunction userHasImage(content: unknown): boolean {\r\n if (!Array.isArray(content)) return false\r\n return content.some(block => block !== null && typeof block === 'object'\r\n && (block as { type?: unknown }).type === 'image')\r\n}\r\n\r\n// ---- Chat-node data plane (DSH 0.1.2-alpha.2+) ----\r\n// DSH 0.1.2-alpha.2 moved Conversation target data out of the Session\r\n// snapshot: `session.getSnapshot()` no longer carries `chat.nodes`, and the\r\n// per-Session Chat nodes live in the uiConversation `'chat'` view target\r\n// (ChatSnapshot with a keyed `nodes` store). The rail resolves nodes from\r\n// either plane — current DSH first, legacy Session snapshot as fallback.\r\n\r\n/** Observable snapshot surface of one Session's `'chat'` view target. */\r\nexport interface ChatViewSource {\r\n subscribe(listener: () => void): () => void\r\n getSnapshot(): unknown\r\n}\r\n\r\n/** Keyed Chat node store published by the official ChatSnapshot (`nodes`). */\r\ninterface ChatNodeStoreLike {\r\n get(key: string): unknown\r\n values(): Iterable<unknown> | readonly unknown[]\r\n}\r\n\r\n/** Structural shape of the uiConversation `'chat'` target snapshot. */\r\ninterface ChatSnapshotLike {\r\n readonly nodes: ChatNodeStoreLike\r\n}\r\n\r\n/** Minimal observable shape of a chat node (either data plane). */\r\ninterface ChatNodeLike {\r\n readonly key?: unknown\r\n readonly kind?: unknown\r\n readonly anchorSeq?: unknown\r\n readonly data?: { time?: unknown; content?: unknown } | null\r\n}\r\n/** Runtime face of the official Conversation assembly service\r\n * (DSH 0.1.2-alpha.2+). Kept as a local structural type because the npm\r\n * client-runtime types (0.1.1-rc.1) predate the service rename and still\r\n * expose the legacy registry names. */\r\ninterface UiConversationRuntime {\r\n binding(sessionId: string): {\r\n target(name: 'chat'): ChatViewSource | undefined\r\n }\r\n}\r\n\r\n/**\r\n * Resolve one chat node from either data plane: the official Chat view\r\n * snapshot (`nodes` keyed store) on current DSH, or the legacy Session\r\n * snapshot's `chat.nodes` Map on older DSH.\r\n * @param snapshot - the active node snapshot.\r\n * @param key - the Conversation Context key.\r\n * @returns the raw node, when its window covers the key.\r\n */\r\nexport function chatNodeOf(snapshot: unknown, key: string | undefined): unknown {\r\n if (key === undefined || snapshot === undefined || snapshot === null) return undefined\r\n const store = (snapshot as ChatSnapshotLike | undefined)?.nodes as ChatNodeStoreLike | undefined\r\n if (store !== undefined && typeof store.get === 'function') {\r\n return store.get(key)\r\n }\r\n const map = (snapshot as { chat?: { nodes?: Map<string, unknown> } } | undefined)?.chat?.nodes\r\n return map?.get(key)\r\n}\r\n\r\n/** Map one chat node (either plane) to a rail message entry. */\r\nexport function railMessageOfNode(node: unknown): RailMessage | null {\r\n if (node === null || typeof node !== 'object') return null\r\n const n = node as ChatNodeLike\r\n if (n.kind !== 'user' && n.kind !== 'steering') return null\r\n const key = typeof n.key === 'string' ? n.key : undefined\r\n if (key === undefined) return null\r\n const data = n.data\r\n if (data === null || typeof data !== 'object') return null\r\n if (typeof data.time !== 'number' || !Array.isArray(data.content)) return null\r\n const images = nodeImagesOf(data.content)\r\n return {\r\n seq: typeof n.anchorSeq === 'number' ? n.anchorSeq : 0,\r\n time: data.time,\r\n text: userTextOf(data.content),\r\n hasImage: userHasImage(data.content),\r\n ...(images.length > 0 ? { images } : {}),\r\n key,\r\n }\r\n}\r\n\r\n/** Fallback collector: enumerate user messages from the loaded chat nodes.\r\n * Accepts either data plane — the official Chat view snapshot (`nodes`\r\n * values) or the legacy Session snapshot's `chat.nodes` Map. */\r\nexport function collectFromNodes(snapshot: unknown): RailMessage[] {\r\n const out: RailMessage[] = []\r\n const store = (snapshot as ChatSnapshotLike | undefined)?.nodes as ChatNodeStoreLike | undefined\r\n if (store !== undefined && typeof store.values === 'function') {\r\n for (const node of store.values()) {\r\n const m = railMessageOfNode(node)\r\n if (m !== null) out.push(m)\r\n }\r\n } else {\r\n const chat = (snapshot as { chat?: { nodes?: Map<unknown, unknown> } } | undefined)?.chat\r\n if (chat?.nodes === undefined) return out\r\n for (const node of chat.nodes.values()) {\r\n const m = railMessageOfNode(node)\r\n if (m !== null) out.push(m)\r\n }\r\n }\r\n out.sort((a, b) => a.seq - b.seq)\r\n return out\r\n}\r\n\r\n/** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */\r\nfunction anchorKeyOf(m: RailMessage): string | undefined {\r\n if (typeof m.key === 'string' && m.key !== '') return m.key\r\n if (typeof m.id === 'string' && m.id !== '') return '13:input-message' + m.id\r\n return undefined\r\n}\r\n\r\n/** Full text of a rail message from the loaded chat nodes (uncapped),\r\n * falling back to the projection preview when the node is not mounted. */\r\nfunction fullTextOf(m: RailMessage, nodeSnapshot: unknown): string {\r\n const key = anchorKeyOf(m)\r\n const node = chatNodeOf(nodeSnapshot, key) as { data?: { content?: unknown } } | undefined\r\n const content = node?.data?.content\r\n if (Array.isArray(content)) {\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n const full = out.trim()\r\n if (full !== '') return full\r\n }\r\n return m.text\r\n}\r\n\r\n/** Displayable tip images for one rail message: host projection references\r\n * first (works even before the node window covers the message), then the\r\n * loaded chat node's content blocks. */\r\nfunction tipImagesOf(m: RailMessage, nodeSnapshot: unknown): ImageSpec[] {\r\n if (Array.isArray(m.images) && m.images.length > 0) {\r\n return m.images.map(img => ({ kind: 'ref', attachmentId: img.attachmentId, mediaType: img.mediaType }))\r\n }\r\n const key = anchorKeyOf(m)\r\n const node = chatNodeOf(nodeSnapshot, key) as { data?: { content?: unknown } } | undefined\r\n return imageSpecsOfContent(node?.data?.content)\r\n}\r\n\r\nconst delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\r\n\r\n/**\r\n * Ensure the message node is loaded into the visible window, then scroll to it.\r\n *\r\n * DSH 0.1.2-alpha.3+ (session-controller) pages history through the exact\r\n * `loadThrough(seq)` jump pager (documented as the turn-jump loader, 200\r\n * messages per page): when the message's event `seq` is known the rail jumps\r\n * with one precise `loadThrough` call and settles its completion, instead of\r\n * paging `loadOlder` (50 messages per page, DSH PAGE_MESSAGES) from the window\r\n * head — a far jump in a long session costs one call, not dozens.\r\n *\r\n * `loadThrough` returns immediately when another loader owns the busy flag\r\n * (the official caller retries once it settles), so the rail waits for the\r\n * owner to release before issuing the jump; after it resolves, the chat view\r\n * assembles the paged window asynchronously, so the target node is polled\r\n * before the row, and the row before the scroll.\r\n *\r\n * Fallback (older DSH without `loadThrough`, or a message without a dependable\r\n * seq): the `loadOlder` loop below, with its page-count progress. \"Loaded\" is\r\n * decided through `nodeOf` — the uiConversation `'chat'` target on current\r\n * DSH, where the Session snapshot no longer carries chat.nodes — falling back\r\n * to the legacy Session snapshot's `chat.nodes` Map without it.\r\n */\r\nasync function jumpToMessage(\r\n sessionsService: { binding: (id: string) => { session?: { getSnapshot(): unknown; loadOlder(): Promise<unknown>; hasMore?: boolean; loadingOlder?: boolean } } | undefined },\r\n sessionId: string,\r\n key: string,\r\n /** Live chat-node reader: called with the current snapshot on every probe. */\r\n nodeOf?: (key: string) => unknown,\r\n onProgress?: (pages: number) => void,\r\n signal?: AbortSignal,\r\n /** Durable event seq of the target message (chatRail projection / node anchor seq). */\r\n targetSeq?: number,\r\n /** Jump-phase signal: `'paging'` when history is actually being loaded in\r\n * (the rail's busy indicator), `'landed'` once the target row is located\r\n * and scrolling is about to run (indicator goes away even while the smooth\r\n * scroll settles — a near mark must never flash \"loading\"). */\r\n onJump?: (phase: 'paging' | 'landed') => void,\r\n): Promise<boolean> {\r\n const session = sessionsService.binding(sessionId)?.session\r\n if (session === undefined) return false\r\n const isLoaded = nodeOf === undefined\r\n ? (k: string) => (session.getSnapshot() as { chat?: { nodes?: Map<string, unknown> } }).chat?.nodes?.get(k) !== undefined\r\n : nodeOf\r\n let pages = 0\r\n let loaded = false\r\n const jumpSession = session as { loadThrough?: ((seq: number) => Promise<unknown>) | undefined }\r\n if (typeof jumpSession.loadThrough === 'function' && targetSeq !== undefined && Number.isSafeInteger(targetSeq)) {\r\n // A plain pull owns the busy flag during its single page; loadThrough does\r\n // not queue behind it, so wait for the owner instead of returning a false\r\n // \"done\" on the first probe.\r\n if (!isLoaded(key)) {\r\n let spin = 0\r\n while (spin++ < 120) {\r\n if (signal?.aborted) return false\r\n const snapshot = session.getSnapshot() as { loadingOlder?: boolean } | undefined\r\n if (snapshot?.loadingOlder !== true) break\r\n await delay(50)\r\n }\r\n if (signal?.aborted) return false\r\n onJump?.('paging')\r\n await jumpSession.loadThrough(targetSeq)\r\n // The pager settles when the window covers targetSeq (or history is\r\n // exhausted), but the chat view assembles the paged window asynchronously:\r\n // poll the node before concluding, bounded the same way as the DOM poll.\r\n let spinNode = 0\r\n while (spinNode++ < 100) {\r\n if (signal?.aborted) return false\r\n if (isLoaded(key)) { loaded = true; break }\r\n await delay(50)\r\n }\r\n if (!loaded) {\r\n console.warn(`[chat-rail] jumpToMessage: node \"${key}\" not loaded after loadThrough(${String(targetSeq)})`)\r\n return false\r\n }\r\n }\r\n // Target already inside the visible window: no paging (official\r\n // TurnNavigator behaviour — near marks scroll in place), so a click on a\r\n // close message lands immediately instead of re-running the jump loader.\r\n loaded = true\r\n } else {\r\n let guard = 0\r\n let reportedPaging = false\r\n while (guard++ < 120) {\r\n if (signal?.aborted) return false\r\n const snapshot = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean } | undefined\r\n if (isLoaded(key)) {\r\n loaded = true\r\n break\r\n }\r\n if (snapshot?.hasMore !== true) break\r\n if (snapshot.loadingOlder === true) {\r\n // Another loader owns the current page; wait for it without busy-spinning.\r\n await delay(50)\r\n continue\r\n }\r\n if (!reportedPaging) { onJump?.('paging'); reportedPaging = true }\r\n await session.loadOlder()\r\n pages++\r\n // Report progress only on page boundaries, not every loop iteration.\r\n onProgress?.(pages)\r\n }\r\n // Page guard exhausted (or hasMore ran out) without the node materializing:\r\n // fail fast with context instead of burning the DOM poll below.\r\n if (!loaded) {\r\n console.warn(`[chat-rail] jumpToMessage: node \"${key}\" not loaded after ${pages} page(s)`)\r\n return false\r\n }\r\n }\r\n const scrollport = typeof document !== 'undefined' ? document.querySelector('[data-conversation-scroll]') : null\r\n if (scrollport === null) return false\r\n // The snapshot is authoritative for \"loaded\", but the row's DOM node only\r\n // appears once React renders the prepended page. Poll for it (up to ~5s) so a\r\n // freshly paged-in jump scrolls on the same click.\r\n let row: Element | null = null\r\n let waited = 0\r\n while (waited++ < 100) {\r\n if (signal?.aborted) return false\r\n row = scrollport.querySelector(`[data-chat-anchor-key=\"${CSS.escape(key)}\"]`)\r\n if (row !== null) break\r\n await delay(50)\r\n }\r\n if (row === null) return false\r\n // Row located: scrolling is the visible feedback — drop the busy indicator\r\n // here even though the smooth scroll (and its re-verify steps) still run.\r\n onJump?.('landed')\r\n const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\r\n // DSH 0.1.2-alpha.3+ paging compensates the reader position while the newly\r\n // prepended window re-flows — a scrollIntoView issued right after a multi-\r\n // page loadThrough lands mid-flight and gets overridden by the compensation\r\n // (verified on the alpha.4 kernel: 145971 → 44464 snap-back). Wait for the\r\n // scrollport geometry to go quiet, land with an explicit scrollTo computed\r\n // from the row's viewport rect, then re-verify a few times: a stray\r\n // bottom-follow can still pull the flight back.\r\n const scrollToRow = (): void => {\r\n const viewRect = scrollport.getBoundingClientRect()\r\n const rowRect = (row as Element).getBoundingClientRect()\r\n // Clamp at the top edge instead of bailing out: the FIRST message's row\r\n // sits at the document head, where the centred target is negative — the\r\n // old `if (target < 0) return` silently dropped the jump and made every\r\n // click on the first rail item a no-op.\r\n const target = Math.max(\r\n 0,\r\n scrollport.scrollTop + (rowRect.top - viewRect.top) - (viewRect.height - rowRect.height) / 2,\r\n )\r\n scrollport.scrollTo({ top: target, behavior: reducedMotion ? 'auto' : 'smooth' })\r\n }\r\n const rowDelta = (): number => {\r\n const viewRect = scrollport.getBoundingClientRect()\r\n const rowRect = (row as Element).getBoundingClientRect()\r\n return Math.abs((rowRect.top + rowRect.height / 2) - (viewRect.top + viewRect.height / 2))\r\n }\r\n let stableTop = -1\r\n let stableHeight = -1\r\n let stability = 0\r\n let quiet = 0\r\n while (quiet++ < 30) {\r\n if (signal?.aborted) return false\r\n const top = scrollport.scrollTop\r\n const height = scrollport.scrollHeight\r\n if (top === stableTop && height === stableHeight) stability++\r\n else { stableTop = top; stableHeight = height; stability = 0 }\r\n if (stability >= 3) break\r\n await delay(150)\r\n }\r\n if (signal?.aborted) return false\r\n scrollToRow()\r\n for (let attempt = 0; attempt < 4; attempt++) {\r\n await delay(600)\r\n if (signal?.aborted) return false\r\n if (rowDelta() <= Math.max(120, scrollport.getBoundingClientRect().height * 0.2)) break\r\n scrollToRow()\r\n }\r\n return true\r\n}\r\n\r\n// ---- tip image thumbnails ----\r\n\r\n/** Session-scoped browser URL cache for resolved attachment bytes (one entry\r\n * per session:attachment; a failed load evicts itself so the next hover\r\n * retries instead of showing a permanently broken thumbnail). */\r\nconst thumbUrls = new Map<string, Promise<string>>()\r\n\r\n/** Minimal RpcResult shape of session.readAttachment (contract level). */\r\ninterface ReadAttachmentResult {\r\n ok: boolean\r\n value?: { attachment?: { mediaType?: string }; data?: Uint8Array }\r\n error?: { code?: string; message?: string }\r\n}\r\n\r\ntype ReadAttachmentFn = (attachmentId: string) => Promise<ReadAttachmentResult>\r\n\r\n/** Fallback data URL when the browser cannot mint object URLs. */\r\nfunction bytesToDataUrl(data: Uint8Array, mediaType: string): string {\r\n let binary = ''\r\n const chunk = 0x8000\r\n for (let offset = 0; offset < data.length; offset += chunk) {\r\n binary += String.fromCharCode(...data.subarray(offset, offset + chunk))\r\n }\r\n return `data:${mediaType};base64,${btoa(binary)}`\r\n}\r\n\r\n/** Resolve (and cache) one stored image to a displayable URL. */\r\nfunction resolveThumb(sessionId: string, read: ReadAttachmentFn, image: Pick<RailImage, 'attachmentId' | 'mediaType'>): Promise<string> {\r\n const key = `${sessionId}:${image.attachmentId}`\r\n let pending = thumbUrls.get(key)\r\n if (pending === undefined) {\r\n pending = read(image.attachmentId).then((result): string => {\r\n if (!result.ok || result.value === undefined) {\r\n throw new Error(result.error?.message ?? result.error?.code ?? 'readAttachment failed')\r\n }\r\n const data = result.value.data\r\n if (data === undefined) throw new Error('readAttachment resolved no bytes')\r\n const mediaType = result.value.attachment?.mediaType ?? image.mediaType\r\n if (typeof URL.createObjectURL === 'function') {\r\n return URL.createObjectURL(new Blob([data as BlobPart], { type: mediaType }))\r\n }\r\n return bytesToDataUrl(data, mediaType)\r\n })\r\n void pending.catch(() => { thumbUrls.delete(key) })\r\n thumbUrls.set(key, pending)\r\n }\r\n return pending\r\n}\r\n\r\n/** One tip thumbnail: resolves its attachment lazily, shows a pulsing\r\n * placeholder while loading, and removes itself on failure. */\r\nfunction TipThumb({ spec, sessionId, read }: {\r\n spec: ImageSpec\r\n sessionId: string\r\n read: ReadAttachmentFn\r\n}): ReactNode {\r\n const [src, setSrc] = useState<string | null>(null)\r\n const [failed, setFailed] = useState(false)\r\n const specKey = spec.kind === 'ref' ? `ref:${spec.attachmentId}` : 'data'\r\n useEffect(() => {\r\n let alive = true\r\n setSrc(null)\r\n setFailed(false)\r\n if (spec.kind === 'data') {\r\n setSrc(spec.src)\r\n return () => { alive = false }\r\n }\r\n resolveThumb(sessionId, read, spec)\r\n .then((url) => { if (alive) setSrc(url) })\r\n .catch(() => { if (alive) setFailed(true) })\r\n return () => { alive = false }\r\n // spec is a fresh object each render; the string key is the stable identity.\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [specKey, sessionId, read])\r\n if (failed) return null\r\n if (src === null) return createElement('div', { className: S.tipImgPh, 'aria-hidden': true })\r\n return createElement('img', { className: S.tipImg, src, alt: '', draggable: false })\r\n}\r\n\r\n// ---- component ----\r\ninterface TimelineRailProps {\r\n useProjection: (key: string) => { messages?: unknown[] } | undefined\r\n sessionId?: SessionId\r\n sessionsService: ISessions\r\n /** Live Chat view source for the session (current DSH `'chat'` target). */\r\n chatOf?: (sessionId: string) => ChatViewSource | undefined\r\n /** Draft write + attachment-add face (session-scope framework injection). */\r\n inputActions?: {\r\n setDraft(text: string): void\r\n addImages(ids: readonly string[]): boolean\r\n } | undefined\r\n /** Runtime draft-image registry (hosted by ui-conversation). */\r\n conversation?: {\r\n createDraftImages(files: readonly File[]): readonly { id: string }[]\r\n } | undefined\r\n}\r\n\r\n/** Resolve copy for the current UI language (document lang, DSH sets zh/en). */\r\nfunction langStrings(): Record<string, string> {\r\n const lang = typeof document !== 'undefined' ? (document.documentElement.lang || 'zh').toLowerCase() : 'zh'\r\n return STRINGS[lang.startsWith('zh') ? 'zh' : 'en']\r\n}\r\n\r\nfunction TimelineRail({ useProjection, sessionId, sessionsService, chatOf, inputActions, conversation }: TimelineRailProps): ReactNode {\r\n const t = langStrings()\r\n const projected = useProjection('chatRail')\r\n const session = sessionId === undefined ? undefined : (sessionsService.binding(sessionId) as { session?: { subscribe(cb: () => void): () => void; getSnapshot(): unknown } } | undefined)?.session\r\n // Chat-node source: the official `'chat'` view target on current DSH (the\r\n // Session snapshot no longer carries chat.nodes); the Session snapshot stays\r\n // as the legacy fallback on older DSH.\r\n const chatSource = sessionId === undefined ? undefined : chatOf?.(sessionId)\r\n const fallbackStore = chatSource ?? (session === undefined ? NOOP_STORE : session)\r\n const nodeSnapshot = useSyncExternalStore(\r\n (cb) => fallbackStore.subscribe(cb),\r\n () => fallbackStore.getSnapshot(),\r\n )\r\n\r\n let messages: RailMessage[] = []\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) {\r\n messages = projected.messages.map(normalize).filter((m): m is RailMessage => m !== null)\r\n }\r\n if (messages.length === 0) {\r\n messages = collectFromNodes(nodeSnapshot)\r\n }\r\n\r\n // 「使用官方轮次导航条」(对比模式,2026-09-05):默认关(chat-rail 主导、隐藏官方);打开 =\r\n // 官方 TurnNavigator 显示 + 本 rail 隐藏 + 行内按钮清理。设置来自官方 settingsScope(设置——\r\n // 插件页卡片),切换即时生效;rail 与卡片订阅同一 scope。\r\n const railSettingsSnap = useSyncExternalStore(\r\n railSettingsScope !== null ? (cb: () => void) => railSettingsScope!.subscribe(cb) : NOOP_SUB,\r\n () => railSettingsScope?.getSnapshot() ?? { value: undefined },\r\n )\r\n const showOfficial = railSettingsSnap.value?.showOfficialNavigator === true\r\n const applyOfficialMode = (on: boolean): void => {\r\n syncOfficialHide(on)\r\n if (on) {\r\n clearRowActions()\r\n if (navRef.current !== null) navRef.current.style.display = 'none'\r\n } else if (navRef.current !== null) {\r\n navRef.current.style.display = ''\r\n }\r\n }\r\n useEffect(() => { applyOfficialMode(showOfficial) }, [showOfficial])\r\n\r\n // Favorites: per-session persisted set + a \"bookmarks only\" filter that\r\n // narrows the rail to favorited messages (mirrors dsh-milestone's\r\n // bookmarksOnly toggle; the DOM-injected buttons share the same store).\r\n const favorites = useSyncExternalStore(subscribeFavorites, favoritesSnapshot)\r\n const [favOnly, setFavOnly] = useState(false)\r\n // Count of favorited messages in THIS session. The filter toggle is only\r\n // worth showing when a favorite exists — with zero favorites there is\r\n // nothing to filter to, so the pill disappears entirely.\r\n const favCount = sessionId === undefined\r\n ? 0\r\n : messages.filter((m) => isFavorite(favorites, sessionId, favoriteIdOfMessage(m))).length\r\n const showFavToggle = favCount > 0\r\n const effectiveMessages = favOnly\r\n ? messages.filter((m) => sessionId !== undefined && isFavorite(favorites, sessionId, favoriteIdOfMessage(m)))\r\n : messages\r\n\r\n // If the filter is armed and the last favorite is un-starred (or the\r\n // session switched), the filtered list would be empty AND the toggle would\r\n // vanish — leaving a rail with nothing to un-filter. Auto-release the\r\n // filter instead so the full list returns.\r\n useEffect(() => {\r\n if (favOnly && !showFavToggle) setFavOnly(false)\r\n }, [favOnly, showFavToggle])\r\n\r\n const [activeIndex, setActiveIndex] = useState(-1)\r\n const [show, setShow] = useState(false)\r\n const [jumping, setJumping] = useState(false)\r\n // Hover full-content panel: index of the hovered item + fixed position.\r\n // The tip only triggers after the expand animation settles — in the\r\n // collapsed state the item rect is only 36px wide, so a tip positioned\r\n // there would be wrong once the rail expands.\r\n const [tip, setTip] = useState<{ index: number; x: number; y: number } | null>(null)\r\n const navRef = useRef<HTMLDivElement | null>(null) // True only after the expand animation has fully settled; the width\r\n // transition takes ~250ms after `show` flips, and item rects are only\r\n // stable once it finishes. Tip positioning must wait for this.\r\n const expandedRef = useRef(false)\r\n // Last observed pointer position over the rail. A mouseenter that arrives\r\n // before the width transition cannot show the tip (rects unstable), yet the\r\n // enter event is already gone by the time the animation ends — the settle\r\n // handler re-hit-tests the cursor against this position to show the tip for\r\n // the node still under the pointer.\r\n const lastPointerRef = useRef<{ x: number; y: number } | null>(null)\r\n // Aborts an in-flight jump (component unmount or a newer click superseding\r\n // an older one), so the loadOlder/DOM-poll loops stop promptly.\r\n const jumpAbortRef = useRef<AbortController | null>(null)\r\n useEffect(() => () => jumpAbortRef.current?.abort(), [])\r\n\r\n // Track the cursor over the rail for the expand-settled tip re-hit-test\r\n // (the `show` effect's settle handler reads lastPointerRef). Native\r\n // listener: plain DOM events keep the callback typed without React\r\n // synthetic-event generics in the hand-written createElement props.\r\n useEffect(() => {\r\n const el = navRef.current\r\n if (el === null) return\r\n const onMove = (e: PointerEvent) => { lastPointerRef.current = { x: e.clientX, y: e.clientY } }\r\n el.addEventListener('pointermove', onMove, { passive: true })\r\n return () => el.removeEventListener('pointermove', onMove)\r\n }, [])\r\n\r\n /** Position the tip against the item's CURRENT (post-expand) rect. */\r\n const positionTip = (index: number) => {\r\n if (index < 0) return\r\n const el = navRef.current?.querySelector<HTMLElement>(`[data-crl-index=\"${index}\"]`)\r\n if (el === null || el === undefined) return\r\n const rect = el.getBoundingClientRect()\r\n setTip({ index, x: rect.left - 12, y: rect.top })\r\n }\r\n\r\n /** Handle item hover: the collapsed state only expands the rail; the tip\r\n * only triggers once the expand animation has settled (item rects are then\r\n * stable, so the tip is positioned correctly). */\r\n const handleItemEnter = (index: number) => {\r\n if (!expandedRef.current) {\r\n setShow(true)\r\n return\r\n }\r\n positionTip(index)\r\n }\r\n\r\n const handleItemLeave = (index: number) => {\r\n setTip((prev) => (prev?.index === index ? null : prev))\r\n }\r\n\r\n // Track the settled expand state: when `show` flips true, wait for the width\r\n // transition to end before allowing tip triggers; when it flips false, the\r\n // rail is collapsing — treat as unexpanded immediately.\r\n useEffect(() => {\r\n if (!show) {\r\n expandedRef.current = false\r\n return\r\n }\r\n const el = navRef.current\r\n if (el === null) return\r\n let settled = false\r\n const settle = () => {\r\n if (settled) return\r\n settled = true\r\n expandedRef.current = true\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n // Re-hit-test the cursor: a mouseenter that fired before the width\r\n // transition could not show the tip (rects were still unstable), and\r\n // the enter event is long gone once the animation ends. If the pointer\r\n // still sits on a rail item, position that item's tip now.\r\n const p = lastPointerRef.current\r\n if (p !== null) {\r\n const hit = document.elementFromPoint(p.x, p.y)\r\n const item = hit === null ? null : hit.closest('[data-crl-index]')\r\n const idx = item === null ? -1 : Number(item.getAttribute('data-crl-index'))\r\n if (Number.isInteger(idx) && idx >= 0) positionTip(idx)\r\n }\r\n }\r\n const onTransitionEnd = (e: TransitionEvent) => {\r\n if (e.propertyName !== 'width') return\r\n settle()\r\n }\r\n el.addEventListener('transitionend', onTransitionEnd)\r\n // Fallback for `prefers-reduced-motion: reduce` (the stylesheet disables\r\n // the width transition, so `transitionend` never fires) and for any lost\r\n // event: settle after the transition duration regardless.\r\n const timer = setTimeout(settle, 300)\r\n return () => {\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n }\r\n }, [show])\r\n\r\n // Session-scoped attachment reader for tip thumbnails (stable identity so\r\n // TipThumb effects do not re-run every render; resolveThumb caches anyway).\r\n const readThumb = useMemo<ReadAttachmentFn | undefined>(() => {\r\n if (sessionId === undefined) return undefined\r\n const s = (sessionsService.binding(sessionId) as unknown as\r\n { session?: { readAttachment?: ReadAttachmentFn } } | undefined)?.session\r\n return s?.readAttachment === undefined\r\n ? undefined\r\n : (id: string) => s.readAttachment!(id)\r\n }, [sessionId, sessionsService])\r\n\r\n // Fill-to-input (plus button): put the message's full text into the draft\r\n // and re-attach its stored images through the official draft-image path\r\n // (readAttachment → File → createDraftImages → addImages). Degrades to\r\n // text-only when the conversation service or storage read is unavailable.\r\n const fillToInput = useCallback((messageId: string) => {\r\n const m = messages.find((candidate) => favoriteIdOfMessage(candidate) === messageId)\r\n if (m === undefined || inputActions === undefined || sessionId === undefined) return\r\n const text = fullTextOf(m, nodeSnapshot) || m.text\r\n if (inputActions.setDraft !== undefined) inputActions.setDraft(text)\r\n const specs = tipImagesOf(m, nodeSnapshot).filter((spec): spec is ImageSpec & { kind: 'ref' } => spec.kind === 'ref')\r\n if (specs.length === 0 || conversation === undefined || readThumb === undefined) return\r\n void (async () => {\r\n const files: File[] = []\r\n for (const spec of specs) {\r\n try {\r\n const result = await readThumb(spec.attachmentId) as unknown as { ok?: boolean; value?: { attachment?: { mediaType?: string }; data?: Uint8Array } }\r\n if (result?.ok !== true || result.value?.data === undefined) continue\r\n const mediaType = result.value.attachment?.mediaType ?? spec.mediaType\r\n files.push(new File([result.value.data as BlobPart], `attachment-${spec.attachmentId}${mediaType.includes('jpeg') ? '.jpg' : '.png'}`, { type: mediaType }))\r\n } catch {\r\n // One failed attachment must not block the others.\r\n }\r\n }\r\n if (files.length === 0) return\r\n const drafts = conversation.createDraftImages(files)\r\n const ids = drafts.map((draft) => draft.id).filter((id): id is string => id !== undefined)\r\n if (ids.length > 0) {\r\n try { inputActions.addImages?.(ids) } catch { /* draft-image registry may reject; text fill already landed */ }\r\n }\r\n })()\r\n }, [conversation, inputActions, messages, nodeSnapshot, readThumb, sessionId])\r\n\r\n // Bridge the DOM-injected buttons (module-level actionCtx) to this\r\n // component's live closures + session id; refresh star states on every\r\n // favorites change and on session switch.\r\n useEffect(() => {\r\n actionCtx.sessionId = sessionId\r\n actionCtx.onToggleFavorite = (messageId) => {\r\n if (sessionId !== undefined) toggleFavorite(sessionId, messageId)\r\n }\r\n actionCtx.onFill = fillToInput\r\n refreshAllFavoriteButtons(t)\r\n return () => {\r\n // Only clear what this render set: a newer session may already own the ctx.\r\n if (actionCtx.sessionId === sessionId) actionCtx.sessionId = undefined\r\n }\r\n }, [sessionId, fillToInput, t])\r\n\r\n // Keep the injected DOM buttons in sync with external favorite toggles\r\n // (rail star / another session's store write).\r\n useEffect(() => {\r\n refreshAllFavoriteButtons(t)\r\n }, [favorites, t])\r\n\r\n // Inject star + plus buttons into every user message row; observe DOM\r\n // changes so history paging and re-renders keep them present.\r\n useEffect(() => {\r\n if (typeof document === 'undefined' || document.body === null) return\r\n if (showOfficial) return\r\n if (typeof document !== 'undefined') {\r\n return startActionInjector(t)\r\n }\r\n }, [showOfficial, t])\r\n\r\n // Background full-history load: follow the runtime's authoritative hasMore\r\n // flag, but STOP as soon as the projection delivers.\r\n useEffect(() => {\r\n if (session === undefined) return\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n let cancelled = false\r\n const run = async () => {\r\n let guard = 0\r\n while (!cancelled && guard++ < 120) {\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n const snap = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean }\r\n if (snap?.hasMore !== true) return\r\n if (snap.loadingOlder === true) { await delay(50); continue }\r\n await (session as unknown as { loadOlder(): Promise<unknown> }).loadOlder()\r\n }\r\n }\r\n run().catch(() => {})\r\n return () => { cancelled = true }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, session === undefined ? 'none' : 'ready', Array.isArray(projected?.messages) && projected.messages.length > 0 ? 'have' : 'none'])\r\n\r\n // Track the reading position (active item): nearest row to the 40% viewport line.\r\n useEffect(() => {\r\n if (effectiveMessages.length === 0) return\r\n const indexByKey = new Map<string, number>()\r\n for (let i = 0; i < effectiveMessages.length; i++) {\r\n const key = anchorKeyOf(effectiveMessages[i])\r\n if (key !== undefined) indexByKey.set(key, i)\r\n }\r\n const updateActive = () => {\r\n const sp = document.querySelector('[data-conversation-scroll]')\r\n if (sp === null) return\r\n const rect = sp.getBoundingClientRect()\r\n if (rect.height === 0) return\r\n const line = rect.top + rect.height * 0.4\r\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"]')\r\n let best = -1\r\n let bestDist = Infinity\r\n for (const row of rows) {\r\n const key = row.getAttribute('data-chat-anchor-key')\r\n if (key === null) continue\r\n const idx = indexByKey.get(key) ?? -1\r\n if (idx === -1) continue\r\n const r = row.getBoundingClientRect()\r\n const dist = Math.abs(r.top + r.height / 2 - line)\r\n if (dist < bestDist) { bestDist = dist; best = idx }\r\n }\r\n setActiveIndex(best)\r\n }\r\n updateActive()\r\n const el = document.querySelector('[data-conversation-scroll]')\r\n let scrollTimer: ReturnType<typeof setTimeout> | null = null\r\n const onScroll = () => {\r\n if (scrollTimer !== null) return\r\n scrollTimer = setTimeout(() => { scrollTimer = null; updateActive() }, 60)\r\n }\r\n el?.addEventListener('scroll', onScroll, { passive: true })\r\n const timer = window.setInterval(updateActive, 2000)\r\n return () => {\r\n el?.removeEventListener('scroll', onScroll)\r\n window.clearInterval(timer)\r\n if (scrollTimer !== null) clearTimeout(scrollTimer)\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, effectiveMessages.length])\r\n\r\n // Scroll the rail so the active item stays centered in its visible area:\r\n // follows the conversation's reading position (and re-centers after a\r\n // session switch, where activeIndex is recomputed for the new session).\r\n useEffect(() => {\r\n if (activeIndex < 0) return\r\n const el = navRef.current\r\n if (el === null) return\r\n const item = el.querySelector<HTMLElement>(`[data-crl-index=\"${activeIndex}\"]`)\r\n if (item === null) return\r\n const target = item.offsetTop - el.clientHeight / 2 + item.clientHeight / 2\r\n el.scrollTo({ top: Math.max(0, target), behavior: 'smooth' })\r\n }, [activeIndex])\r\n\r\n // The rail hides on tiny sessions (fewer than 2 user messages) — but the\r\n // count is the UNFILTERED list: the favorites-only filter may legitimately\r\n // narrow to a single row, and hiding the rail then would strand the user\r\n // with no way to switch the filter back off.\r\n if (sessionId === undefined || messages.length < 2) return null\r\n\r\n // Fav-only filter remaps the tip index into the FULL list (tip content and\r\n // hover data always come from the unfiltered messages array).\r\n const tipIndex = tip === null\r\n ? -1\r\n : favOnly\r\n ? messages.findIndex((m) => effectiveMessages[tip.index] !== undefined\r\n && favoriteIdOfMessage(m) === favoriteIdOfMessage(effectiveMessages[tip.index]))\r\n : tip.index\r\n\r\n const items = effectiveMessages.map((m, i) => {\r\n const key = anchorKeyOf(m)\r\n const starred = favoriteIdOfMessage(m) !== '' && isFavorite(favorites, sessionId, favoriteIdOfMessage(m))\r\n return createElement('button', {\r\n type: 'button',\r\n key: m.seq,\r\n 'data-crl-index': String(i),\r\n className: S.item + (activeIndex === i ? ` ${S.itemActive}` : '') + (starred ? ` ${S.favItem}` : ''),\r\n 'aria-label': `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,\r\n 'aria-current': activeIndex === i ? 'location' : undefined,\r\n disabled: jumping,\r\n onClick: () => {\r\n if (key === undefined || jumping) return\r\n jumpAbortRef.current?.abort()\r\n const controller = new AbortController()\r\n jumpAbortRef.current = controller\r\n void jumpToMessage(\r\n sessionsService as never,\r\n sessionId as string,\r\n key,\r\n (k) => chatNodeOf(fallbackStore.getSnapshot(), k),\r\n undefined,\r\n controller.signal,\r\n // DSH 0.1.2-alpha.3+ jump pager: the message's event seq lets the\r\n // rail page history with one exact loadThrough call instead of a\r\n // loadOlder loop (50 messages per page) from the window head.\r\n m.seq,\r\n // Busy indicator only while history is actually paging in; a near\r\n // mark (already loaded) never flashes \"loading\", and the indicator\r\n // drops as soon as the target row is located — before the smooth\r\n // scroll settles, which is the visible feedback itself.\r\n (phase) => setJumping(phase === 'paging'),\r\n ).finally(() => setJumping(false))\r\n },\r\n onMouseEnter: () => handleItemEnter(i),\r\n onMouseLeave: () => handleItemLeave(i),\r\n children: [\r\n createElement('span', { className: S.num }, `#${i + 1}`),\r\n starred\r\n ? createElement('span', { className: S.favStar, role: 'img', 'aria-label': t.fav, 'aria-hidden': undefined },\r\n createElement('svg', {\r\n viewBox: '0 0 24 24',\r\n width: 13,\r\n height: 13,\r\n fill: 'currentColor',\r\n stroke: 'none',\r\n 'aria-hidden': true,\r\n },\r\n createElement('path', { d: STAR_PATH, 'aria-hidden': true })))\r\n : null,\r\n createElement('span', { className: S.title }, m.text === '' ? t.noText : m.text),\r\n m.hasImage\r\n ? createElement('span', { className: S.img, role: 'img', 'aria-label': t.hasImage },\r\n createElement('svg', {\r\n viewBox: '0 0 16 16',\r\n width: 12,\r\n height: 12,\r\n fill: 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 1.4,\r\n strokeLinecap: 'round',\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n },\r\n createElement('rect', { x: 1.7, y: 2.7, width: 12.6, height: 10.6, rx: 2.2 }),\r\n createElement('circle', { cx: 5.6, cy: 6.4, r: 1.15, fill: 'currentColor', stroke: 'none' }),\r\n createElement('path', { d: 'M2.5 11.6l3.6-3.1 2.6 2.2 2.1-1.9 3.3 2.8' })))\r\n : null,\r\n createElement('span', { className: S.time }, relativeTime(m.time, t)),\r\n createElement('span', { className: S.ind, 'aria-hidden': true },\r\n createElement('span', { className: S.line })),\r\n ],\r\n })\r\n })\r\n\r\n return createPortal(\r\n // Fragment: the nav (transformed) and the tip must be siblings — a fixed\r\n // child inside a transformed ancestor is positioned relative to that\r\n // ancestor, breaking the viewport coordinates the tip uses.\r\n [createElement('div', {\r\n ref: navRef,\r\n className: S.nav + (show ? ` ${S.navShow}` : ''),\r\n role: 'navigation',\r\n 'aria-label': t.railLabel,\r\n onMouseEnter: () => setShow(true),\r\n onMouseLeave: () => setShow(false),\r\n children: [\r\n // Favorites-only filter: a star pill pinned at the top of the rail\r\n // (sticky; it never scrolls away with the rows). On state fills the\r\n // star (yellow) and narrows the rail to favorited messages; the label\r\n // (\"bookmarks only\") only shows once the rail expands — the collapsed\r\n // 36px capsule keeps a clean round icon. Hidden entirely when this\r\n // session has no favorites (nothing to filter to).\r\n showFavToggle\r\n ? createElement('button', {\r\n type: 'button',\r\n key: 'favToggle',\r\n className: S.favToggle + (favOnly ? ' crl_on' : ''),\r\n 'aria-pressed': favOnly,\r\n 'aria-label': t.favOnly,\r\n title: t.favOnly,\r\n onMouseEnter: (e: MouseEvent) => e.stopPropagation(),\r\n onClick: () => setFavOnly((v) => !v),\r\n children: [\r\n createElement('svg', {\r\n viewBox: '0 0 24 24',\r\n width: 14,\r\n height: 14,\r\n fill: favOnly ? 'currentColor' : 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 2,\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n }, createElement('path', { d: STAR_PATH })),\r\n createElement('span', { className: S.favToggleLabel }, t.favOnly),\r\n ],\r\n })\r\n : null,\r\n jumping ? createElement('div', { className: S.loading, key: 'loading' },\r\n createElement('span', { className: S.loadingLabel }, t.loading)) : null,\r\n ...items,\r\n ],\r\n }),\r\n // Full-content panel: anchored to the left of the hovered item. Images\r\n // stack above the text; a message whose image cannot be resolved (no\r\n // projection refs and unloaded node) shows a quiet \"has image\" badge.\r\n tip !== null && tipIndex >= 0 && tipIndex < messages.length\r\n ? createElement('div', {\r\n className: S.tip,\r\n style: { left: `${tip.x}px`, top: `${tip.y}px`, transform: 'translateX(-100%)' },\r\n }, (() => {\r\n const m = messages[tipIndex]\r\n const specs = tipImagesOf(m, nodeSnapshot)\r\n const children: ReactNode[] = []\r\n if (specs.length > 0 && readThumb !== undefined) {\r\n // Only the first image renders; the rest collapse into a \"+N\"\r\n // badge so a many-image message cannot push the tip past its\r\n // viewport cap (the stacked thumbnails did).\r\n children.push(createElement('div', { className: S.tipImgs, key: 'imgs' },\r\n createElement('div', { className: S.tipImgWrap, key: 'img' },\r\n createElement(TipThumb, { spec: specs[0], sessionId: sessionId as string, read: readThumb }),\r\n specs.length > 1\r\n ? createElement('span', { className: S.tipImgCount, 'aria-hidden': true }, `+${specs.length - 1}`)\r\n : null)))\r\n } else if (m.hasImage) {\r\n children.push(createElement('span', { className: S.tipBadge, key: 'badge' }, t.hasImage))\r\n }\r\n children.push(createElement('span', { key: 'text' }, fullTextOf(m, nodeSnapshot) || t.noText))\r\n return children\r\n })())\r\n : null],\r\n document.body,\r\n )\r\n}\r\n\r\nfunction apply(ctx: ClientContext): void {\r\n // Current DSH (0.1.2-alpha.2+) moved Conversation target data out of the\r\n // Session snapshot: chat nodes live in the uiConversation `'chat'` view\r\n // target. Resolved through optional ctx.inject (own fiber, never blocking\r\n // this plugin) so older DSH without the service keeps the legacy Session\r\n // snapshot fallback untouched.\r\n let chatOf: ((sessionId: string) => ChatViewSource | undefined) | undefined\r\n ctx.inject(['uiConversation'], (scope: ClientContext) => {\r\n const ui = (scope as unknown as { uiConversation: UiConversationRuntime }).uiConversation\r\n chatOf = (sessionId: string): ChatViewSource | undefined => {\r\n try {\r\n const binding = ui.binding(sessionId)\r\n return binding.target('chat') as unknown as ChatViewSource | undefined\r\n } catch {\r\n // Unknown session: the rail already gates on a live session id.\r\n return undefined\r\n }\r\n }\r\n })\r\n ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({\r\n name: 'conversation.input.dock',\r\n id: 'chat-rail',\r\n order: 40,\r\n inject: () => ({\r\n sessionsService: ctx.sessions,\r\n chatOf,\r\n // createDraftImages lives on the concrete ConversationController, not\r\n // the outward IConversation face; the runtime service is always the\r\n // controller, so the cast is structural, never a feature guess.\r\n conversation: ctx.conversation as unknown as { createDraftImages(files: readonly File[]): readonly { id: string }[] },\r\n }),\r\n }, TimelineRail))\r\n // 设置(设置——插件页):与 node-appearance 同构——注册「可配置插件」卡片(keyed by namespace)。\r\n ctx.inject(['settingsScope'], (scope) => {\r\n bindRailSettingsScope((scope as unknown as { settingsScope: unknown }).settingsScope)\r\n })\r\n ctx.slots.inject(('settings.plugin.item') as never, () => ctx.slots.register({\r\n name: 'settings.plugin.item',\r\n key: 'chat-rail',\r\n inject: () => ({\r\n setShowOfficialNavigator: (show: boolean) => railSettingsScope?.set('showOfficialNavigator', show),\r\n t: langStrings(),\r\n }),\r\n } as never, ChatRailSettingsRow))\r\n}\r\n\r\n/** 设置——插件页卡片:chat-rail「使用官方轮次导航条」开关(对比模式,即时生效、免重启)。 */\r\nexport interface ChatRailSettingsRowFace {\r\n setShowOfficialNavigator(show: boolean): void\r\n t: Record<string, string>\r\n}\r\n\r\nexport function ChatRailSettingsRow({ setShowOfficialNavigator, t }: ChatRailSettingsRowFace): ReactNode {\r\n const [open, setOpen] = useState(false)\r\n const snap = useSyncExternalStore(\r\n railSettingsScope !== null ? (cb: () => void) => railSettingsScope!.subscribe(cb) : NOOP_SUB,\r\n () => railSettingsScope?.getSnapshot() ?? { value: undefined },\r\n )\r\n const on = snap.value?.showOfficialNavigator === true\r\n return createElement('li', { className: 'crlSetCard' + (open ? ' crlSetCardOpen' : '') },\r\n createElement('button', {\r\n type: 'button',\r\n className: 'crlSetHeader',\r\n 'aria-expanded': open,\r\n onClick: () => setOpen((v) => !v),\r\n children: [\r\n createElement('span', { className: 'crlSetHeadText' },\r\n createElement('span', { className: 'crlSetName' }, t.cardName),\r\n createElement('span', { className: 'crlSetDesc' }, t.cardDesc),\r\n ),\r\n createElement('svg', {\r\n className: 'crlSetChevron' + (open ? ' crlSetChevronOn' : ''),\r\n viewBox: '0 0 14 14', width: 14, height: 14,\r\n fill: 'none', 'aria-hidden': true,\r\n }, createElement('path', {\r\n d: 'M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z',\r\n fill: 'currentColor',\r\n })),\r\n ],\r\n }),\r\n open ? createElement('div', { className: 'crlSetBody', children: [\r\n createElement('div', { className: 'crlSetRow' }, [\r\n createElement('div', { className: 'crlSetRowLabel' },\r\n createElement('span', { className: 'crlSetHint' }, t.officialNavHint),\r\n ),\r\n createElement('button', {\r\n type: 'button',\r\n role: 'switch',\r\n 'aria-checked': on,\r\n className: 'crlSetSwitch' + (on ? ' on' : ''),\r\n onClick: () => setShowOfficialNavigator(!on),\r\n children: createElement('span', { className: 'crlSetKnob' }),\r\n }),\r\n ]),\r\n ] }) : null,\r\n )\r\n}\r\n\r\nexport { apply, TimelineRail, imageSpecsOfContent, normalize, jumpToMessage }\r\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-chat-rail",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "画卷式消息导航栏:右侧竖条 hover 展开画卷,scroll-spy 高亮居中跟随,编号+时间+完整预览,与 better-sidebar 双面板动画同步避让",
5
5
  "type": "module",
6
6
  "main": "lib/index.mjs",