@max-null/dsh-chat-rail 0.6.7 → 0.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/lib/client.js +1 -1
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ This plugin belongs to the **`@max-null/*` family** — a set of plugins that to
|
|
|
40
40
|
- **会话区滚动条隐藏**:会话区(`[data-conversation-scroll]`)自带的 Y 轴滚动条被本插件画成透明——它与导航条的位置指示功能重复,官方内核 `0.1.5` 起又把 bar 从「紧贴内容边缘」改为「离边缘 2px」,观感上更显眼。只覆盖 thumb 颜色、**保留官方 8px 槽位**(`scrollbar-gutter: stable` 是官方为输入卡跨视图不位移而保留的),滚轮/触控板滚动不受影响。
|
|
41
41
|
- **无 better-sidebar 也能用**:变量缺失时走 CSS fallback(垂直居中 + 视口右缘),纯 DSH 环境开箱即用。
|
|
42
42
|
- **与 DSH 自带轮次导航协同**:DSH `0.1.2-alpha.1+` 自带 TurnNavigator(轮次导航竖轨)。安装本插件后自动隐藏官方竖轨(aria-label 双文案锚点,不依赖构建 hash),由本插件统一承担会话导航;未安装本插件时官方导航照常显示。设置卡片另提供**「使用官方轮次导航条(对比模式)」**开关,可随时切回官方竖轨对比(免重启生效)。
|
|
43
|
+
- **不遮挡 DSH 的菜单与浮层**:官方 frame 的浮层梯度是「右侧面板 10 → 拖拽手柄 11 → 标题栏座位 15 → `shell.overlay` 20 → 全屏右面板 40」(正本 `AppFrame.module.css`)。导航条定在 **5**,只压在会话列内容之上、不进浮层那一档——于是菜单与 tooltip(100)、以及 `shell.overlay` 里的应用级浮层(例如插件中心的「插件更新」弹窗)都赢它。
|
|
43
44
|
- **深历史跳转(DSH `0.1.2-alpha.3+` 内核)**:会话历史按窗口分页加载(`hasMore`)时,点击跳转走官方 `loadThrough(seq)` 精确跳转装载器(200 条/页一次到位),替代旧版逐页 `loadOlder`(50 条/页)循环;旧内核无 `loadThrough` 时自动回退逐页循环。
|
|
44
45
|
- **DSH 版本要求**:`>=0.1.7-rc.2 <0.3.0`,与 `peerDependencies` 一致。内核按 `semver.satisfies(运行时, 范围, { includePrerelease: true })` 逐个校验 `@deepseek-ai/dsh-*` peer——预发布版参与比较,caret 写法 `^0.1.7-rc.2` 的上界是 `0.2.0-0`,对 `0.2.0-rc.1` 判 false 会让插件**不被装载**,所以上界写成显式的 `<0.3.0`。
|
|
45
46
|
|
package/lib/client.js
CHANGED
|
@@ -93,7 +93,7 @@ window.__ModuleLoader__.load({
|
|
|
93
93
|
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
94
94
|
}
|
|
95
95
|
const css = [
|
|
96
|
-
".crl_nav{user-select:none;z-index:
|
|
96
|
+
".crl_nav{user-select:none;z-index:5;position:fixed;right:3px;top:calc((100vh - var(--dsh-sidebar-height,0px)) / 2);transform:translateY(-50%);width:36px;max-height:min(60vh,420px,calc(100vh - var(--dsh-sidebar-height,0px) - 40px));display:flex;flex-direction:column;align-items:center;box-sizing:border-box;padding:0 0 10px;border-radius:18px;overflow-y:hidden;overflow-x:hidden;background:rgba(255,255,255,.55);border:1px solid rgba(0,0,0,.07);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);scrollbar-width:none;transition:width .25s cubic-bezier(.4,0,.2,1),right .25s cubic-bezier(.4,0,.2,1),top .25s cubic-bezier(.4,0,.2,1),background .2s ease,border-color .2s ease,box-shadow .2s ease}",
|
|
97
97
|
".crl_list{display:flex;flex-direction:column;align-items:center;width:100%;min-height:0;flex:1 1 auto;position:relative;overflow-y:hidden;overflow-x:hidden;scrollbar-width:none}",
|
|
98
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;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)}",
|
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. POSITION ANCHOR. The rail's right edge follows the conversation column\r\n * (`[data-conversation-scroll]`), not the viewport: the official right\r\n * sidebar pushes the conversation narrower, and the rail must travel with\r\n * it. `--dsh-sidebar-width` reads 0px on the current kernel, so a CSS-only\r\n * offset would pin the rail underneath that sidebar.\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\n// 0.1.7:`@deepseek-ai/dsh-client-runtime` 已被移除(`packages/client` 下再无 runtime\r\n// 目录,npm 上停更于 0.1.1-rc.2)。三种类型各归其位——这也是官方 client 包的统一写法\r\n// (`ui-conversation`、`ui-commands`、`ui-session` 等 163 处均如此):\r\n// · ClientContext → cordis 的 Context 别名\r\n// · SessionId → dsh-session/types(`SessionId = Branded<'SessionId'>`)\r\n// · ISessions → dsh-api-session-controller/client(契约在 client/contract/sessions.ts)\r\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\r\nimport type { SessionId } from '@deepseek-ai/dsh-session/types'\r\nimport type { ISessions } from '@deepseek-ai/dsh-api-session-controller/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// Type-only:Context / SlotMap 增强声明。把 ClientContext 还原成裸 `Context` 之后,\r\n// `ctx.slots`、`ctx.configForms` 以及 `plugins.bundle.config` 的 SlotMap 条目都要靠\r\n// 这些包的 declare module 才能存在(它们不产生运行时代码)。\r\n// `ctx.slots` 的合并来自 **renderer 包**(slot 注册表的属主),不是 ui-slots——\r\n// 后者是 shell 预加载的静态库,没有可安装的 `./client` 入口。\r\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\r\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\r\nimport type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'\r\n\r\nexport const inject = ['slots', 'sessions', 'conversation', 'configForms']\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 由组件按**会话列右缘**计算后用内联样式给出(见 anchor);这里的 3px 只是\r\n// 会话列尚未就绪时的兜底。`--dsh-sidebar-width` 在当前内核恒为 0px,不能用它定位——\r\n// 那会把 rail 钉死在视口右缘,右侧边栏一开就被压在面板下面。\r\nconst css = [\r\n // 展开/收起用 ease-in-out 且两侧对称时长,减少「抖一下」(原来 width 用\r\n // cubic-bezier(.4,0,.2,1)、right/top 用 var(--ds-transition-duration-slow),\r\n // 两条曲线不同步,中间过程会互相追)。\r\n '.crl_nav{user-select:none;z-index:100;position:fixed;right:3px;top:calc((100vh - var(--dsh-sidebar-height,0px)) / 2);transform:translateY(-50%);width:36px;max-height:min(60vh,420px,calc(100vh - var(--dsh-sidebar-height,0px) - 40px));display:flex;flex-direction:column;align-items:center;box-sizing:border-box;padding:0 0 10px;border-radius:18px;overflow-y:hidden;overflow-x:hidden;background:rgba(255,255,255,.55);border:1px solid rgba(0,0,0,.07);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);scrollbar-width:none;transition:width .25s cubic-bezier(.4,0,.2,1),right .25s cubic-bezier(.4,0,.2,1),top .25s cubic-bezier(.4,0,.2,1),background .2s ease,border-color .2s ease,box-shadow .2s ease}',\r\n // 列表容器:rail 自身不再滚动,滚动只发生在这一层,header 因此永远不覆盖条目。\r\n '.crl_list{display:flex;flex-direction:column;align-items:center;width:100%;min-height:0;flex:1 1 auto;position:relative;overflow-y:hidden;overflow-x:hidden;scrollbar-width:none}',\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;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,.crl_list::-webkit-scrollbar{width:0;height:0}',\r\n // 会话区 Y 轴滚动条:官方内核 0.1.5 起把 bar 从「紧贴内容边缘」改为「离边缘 2px」\r\n // (`ConversationRoot.module.css` 的 `margin-right: 2px` + track 内缩),于是一条\r\n // 独立细条落在导航条旁边——与导航条的位置指示功能重复,视觉上是噪声,由本插件\r\n // 统一隐藏(滚轮/触控板滚动不受影响)。\r\n //\r\n // 做法是**把 thumb 画成透明**,而不是把滚动条宽度压成 0:宽度归零会连带回收\r\n // `scrollbar-gutter: stable` 的 8px 槽位,内容区因此变宽、输入卡横移,而官方\r\n // 无条件保留槽位正是为了跨视图时输入卡不位移(`ConversationRoot.module.css` 的\r\n // gutter 注释 + `2026-08-04-composer-tab-gutter-reservation`)。track 官方本来就\r\n // 透明,所以只需这一条;thumb:hover 一并列出,避免特异性与注入顺序决定 hover 态。\r\n //\r\n // 两条边界:① **不写 `scrollbar-width`** —— ui-theme/scrollbar.css 注释记录了实测,\r\n // 非 auto 的 `scrollbar-width` 会让 Chromium 丢弃该元素的全部 `::-webkit-scrollbar*`\r\n // 规则;② 用官方契约锚点 `[data-conversation-scroll]`,不依赖 CSS module hash。\r\n '[data-conversation-scroll]::-webkit-scrollbar-thumb,[data-conversation-scroll]::-webkit-scrollbar-thumb:hover{background:transparent}',\r\n // 列表独立滚动:header(收藏开关)留在流内、不覆盖列表内容。\r\n '.crl_show .crl_list{overflow-y:auto;align-items:stretch}',\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 // 折叠态**居中**:指示线(宽 10px)与上方星标按钮(宽 26px)都以容器中线为轴,\r\n // 二者中心同在一列。此前是 flex-end,指示线被推到右边(中心≈22px)而星标居中\r\n // (18px),用户截图指出的「星和导航条没对齐」即此。\r\n '.crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:center;align-items:center;width:100%;box-sizing:border-box;padding:0 9px;line-height:20px;background:none;border:none;font:inherit;text-align:left;color:rgba(0,0,0,.68);transition:padding .25s cubic-bezier(.4,0,.2,1),color .15s ease}',\r\n // 展开态回到 flex-end:此时要的是「#序号 标题 时间 [指示线]」右端对齐,居中会让文字行变得难读。\r\n '.crl_show .crl_item{justify-content:flex-end;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 // tip 里现在有**可点击项**(提问&回答),所以容器不能再 `pointer-events:none`;\r\n // 同时需要一点存活宽限:鼠标从 rail 移到 tip 的途中会先触发 item 的 mouseleave,\r\n // 否则 tip 在指针到达之前就被关掉,里面的按钮根本点不到(实测:坐标点击无效、\r\n // 而元素 .click() 有效,即为此竞态)。\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:auto}',\r\n // tip 里的「提问&回答」区(用户 2026-09-14 重新设计:问答不进导航条,收在这里)\r\n '.crl_tipQaWrap{margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));display:flex;flex-direction:column;gap:4px;pointer-events:auto}',\r\n '.crl_tipQa{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;padding:5px 8px;border:none;border-radius:7px;background:rgba(77,107,254,.08);color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background .15s ease}',\r\n '.crl_tipQa:hover{background:rgba(77,107,254,.16)}',\r\n '.crl_tipQaIcon{flex:none;display:block;color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n '.crl_tipQaText{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',\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 header action: 「只显示收藏」开关。折叠态是一个圆图标,展开态是整行的\r\n // header(星标 + 文案 + 底部一条分隔线),与下方消息列表分层。\r\n //\r\n // 历史坑(2026-09-14 用户截图反馈):这里曾用 `position:sticky; top:-10px`\r\n // 把按钮上推到胶囊**外面**,代价是圆形 hover 背景骑在面板上边缘之上,看起来\r\n // 像浮在导航条外面。sticky 本身是对的(展开态 `.crl_nav.crl_show` 是\r\n // `overflow-y:auto`,列表会滚,header 必须钉住),错的是那 −10px。\r\n //\r\n // 正确解法:`top:0` 与胶囊内边缘齐平,并给按钮**自己的不透明底色**——滚动行\r\n // 从容器 padding 缝隙经过时被底色挡住,而不是靠把控件挪出容器。展开态再补一条\r\n // 底部分隔线,读起来就是 panel header。\r\n // 折叠态:**居中**,与下方已居中的指示线同轴(此前 align-self:flex-end 是为了\r\n // 迁就 flex-end 的指示线;指示线改居中后,这里也回居中)。\r\n // transition 里必须带 margin/left:rail 宽度在 36px↔280px 之间变化时,居中的\r\n // 按钮靠 margin 自动重算来跟随,加过渡后才是「滑过去」而不是「闪过去」\r\n // (用户明确提出的观感问题)。\r\n // 折叠态与展开态同构:收藏开关都是面板的 header(通栏 + 底部分界线),命令式地\r\n // 去掉原先那块白色圆角底——它在半透明胶囊里是一块突兀的白斑,与展开态的通栏\r\n // 标题栏不是一套语言。折叠态保留顶部 10px 内边距,避开胶囊的半圆头。\r\n '.crl_favToggle{flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:6px;width:34px;height:26px;margin:0 0 6px;padding:0;border:none;border-radius:0;background:transparent;border-bottom:1px solid rgba(0,0,0,.07);color:rgba(0,0,0,.42);cursor:pointer;font-size:11px;line-height:1;white-space:nowrap;transition:background .15s ease,color .15s ease,margin .25s cubic-bezier(.4,0,.2,1),left .25s cubic-bezier(.4,0,.2,1)}',\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 // 展开态:整行 header(宽度与胶囊内宽一致),底部分隔线把它和消息行分开。\r\n // 折叠/展开都用「居中 + 对称边距」,这样宽度变化时位置连续、不跳变。\r\n // 展开态:收藏开关作为通栏标题栏贴到面板顶部——上方不再留内边距与自成的边框,\r\n // 分界由它自己的底边框承担;顶部两角交给 rail 的圆角裁切。\r\n '.crl_show .crl_favToggle{width:100%;height:28px;margin:0 0 6px;padding:0 12px;border-radius:0;justify-content:flex-start;text-align:left;border-bottom:1px solid rgba(0,0,0,.07)}',\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);border-bottom-color:rgba(255,255,255,.1)}',\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_show .crl_favToggle,[data-theme=\\'dark\\'] .crl_show .crl_favToggle,.dark .crl_show .crl_favToggle{border-bottom-color:rgba(255,255,255,.1)}',\r\n // 空间不足(会话区被右侧边栏遮到没有参考价值)时整体退场。\r\n // 用 opacity + pointer-events 而不是 display:none —— 官方模式的切换要能直接\r\n // 复用 nav 的内联 display,两者不该互相覆盖。\r\n // 选择器带 .crl_nav 提特异性:单类版本实测只有 pointer-events 生效、opacity 未淡出。\r\n '.crl_nav.crl_navHidden{opacity:0 !important;pointer-events:none !important}',\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/**\r\n * `data-plugin` 归属标记:DSH 的 client 模块系统只把**没有** `data-plugin` 的 `<style>`\r\n * 认领给正在 materialize 的模块(`claimStyles`),而 HMR 重载时按\r\n * `data-plugin === 模块 id` 逐字匹配删除。裸注入的 style 会被任意模块认领走,\r\n * 随后随那个模块的一次重载被物理删除——元素还在、样式消失(悬浮球样式事故的机制)。\r\n * 这里用一个独立于模块 id 的稳定值:既不进别人的账,也不随本模块的进出被清掉。\r\n */\r\nconst HIDE_OFFICIAL_PLUGIN_TAG = 'dsh-chat-rail-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.setAttribute('data-plugin', HIDE_OFFICIAL_PLUGIN_TAG)\r\n tag.textContent = HIDE_OFFICIAL_CSS\r\n document.head.appendChild(tag)\r\n } else if (el.textContent !== HIDE_OFFICIAL_CSS) {\r\n // 同主样式:内容比对,避免 HMR 重载后旧内容占住标签\r\n el.textContent = HIDE_OFFICIAL_CSS\r\n }\r\n}\r\n\r\n/**\r\n * 会话区剩余宽度低于此值时隐藏导航条(px)。\r\n *\r\n * 36px 是 rail 自身宽度,留 ~220px 给消息区——再窄时指示点已无参考意义。\r\n */\r\nconst RAIL_MIN_SPACE = 260\r\n\r\n/** 导航条隐藏类(空间不足时由 React 加上;用类而非内联 display,避免与官方模式冲突)。 */\r\nconst NAV_HIDDEN_CLASS = 'crl_navHidden'\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// 配置表单(0.1.7 起以 `ctx.configForms.get(entryId)` 取代 settingsScope 服务)。\r\ntype RailSettings = { showOfficialNavigator?: boolean }\r\nlet railSettingsForm: {\r\n getSnapshot(): { value?: RailSettings }\r\n subscribe(listener: () => void): () => void\r\n set(key: 'showOfficialNavigator', value: boolean): Promise<unknown>\r\n} | null = null\r\n\r\n/**\r\n * client apply 时绑定(由 `ctx.configForms.get('chat-rail')` 取得)。\r\n *\r\n * 运行时面与旧的 `settingsScope.bind()` 同形:`ConfigForm<T>` 提供 `getSnapshot` /\r\n * `subscribe` / `set(field, value)`(`ui-settings/src/client/config-form-types.ts:39`),\r\n * 所以两处订阅与写入的调用点都不用改。模块级句柄是既有骨架,本次只换来源、不动结构。\r\n * @param form - Host 侧 `configForms` 服务为 `chat-rail` 这个 entry 提供的表单。\r\n */\r\nexport function bindRailSettingsForm(form: unknown): void {\r\n if (typeof form !== 'object' || form === null) return\r\n railSettingsForm = form as typeof railSettingsForm\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\n// 内容比对而非「已存在就跳过」:HMR 重载会重新执行本模块,若沿用旧标签,旧 CSS 会\r\n// 一直占住这个 id,改动看着像\"HMR 生效了\"却仍是旧规则(实测:只能重启内核才看到)。\r\nif (typeof document !== 'undefined') {\r\n const existing = document.head.querySelector<HTMLStyleElement>(`style[data-plugin-css=\"${STYLE_ID}\"]`)\r\n if (existing === 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 } else if (existing.textContent !== css) {\r\n existing.textContent = css\r\n }\r\n}\r\n\r\nconst S = {\r\n nav: 'crl_nav',\r\n navShow: 'crl_show',\r\n list: 'crl_list',\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 tipQaWrap: 'crl_tipQaWrap',\r\n tipQa: 'crl_tipQa',\r\n tipQaIcon: 'crl_tipQaIcon',\r\n tipQaText: 'crl_tipQaText',\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\nexport interface 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 * DOM 锚点键(`data-chat-anchor-key` 的值),与 `key`(chat node key)**不同**。\r\n *\r\n * 问答条目由 `collectQaFromNodes` 设置:它的 `key` 是 tool-call 的 **node key**\r\n * (jumpToMessage 用它判断\"是否已加载\"),而锚点是 `call:<callId>`\r\n * (ui-tool/ToolCallTree.tsx:36 设的那个属性,用来在 DOM 里定位行)。\r\n * 两者混用会导致「永远判定未加载 → 不滚动」。\r\n */\r\n anchor?: string\r\n /**\r\n * 「提问&回答」条目(`ask_user_question` 的 tool-call)。\r\n *\r\n * 它们由 `collectQaFromNodes` 从助手节点收集,与批量投影无关;`key` 是官方\r\n * 工具调用的 DOM 锚点 `call:<callId>`,因此同样能跳转。\r\n */\r\n qa?: boolean\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/** 任一数据平面的节点集合(与 collectFromNodes 同一套取值规则)。 */\r\nfunction nodeValuesOf(snapshot: unknown): unknown[] {\r\n const store = (snapshot as ChatSnapshotLike | undefined)?.nodes as ChatNodeStoreLike | undefined\r\n if (store !== undefined && typeof store.values === 'function') {\r\n return [...store.values()]\r\n }\r\n const chat = (snapshot as { chat?: { nodes?: Map<unknown, unknown> } } | undefined)?.chat\r\n if (chat?.nodes === undefined) return []\r\n return [...chat.nodes.values()]\r\n}\r\n\r\n/**\r\n * 「提问&回答」也要能被导航。\r\n *\r\n * 它**不是** chat node:`ask_user_question` 是助手消息里的一个 tool-call 块,\r\n * 用户的答案落在同一个 callId 的 `tool/result`(实测会话日志:有 `tool/call\r\n * name=ask_user_question` 与对应结果,**没有任何带问答痕迹的 user/message**)。\r\n * 所以 `railMessageOfNode` 的 `kind==='user'` 判定永远覆盖不到它。\r\n *\r\n * 但官方给工具调用也设了 DOM 锚点(`ui-tool/ToolCallTree.tsx:36` →\r\n * `data-chat-anchor-key={`call:${callId}`}`),所以**可跳转**——只要把\r\n * 助手节点里的问答块收集成 rail 条目即可。\r\n *\r\n * 范围:只覆盖已加载的 chat node(不做 loadThrough 拉取未加载历史)——\r\n * 用户 2026-09-14 拍板。\r\n *\r\n * @param snapshot - 当前 chat node 快照(任一平面)。\r\n * @returns 按 seq 升序的问答条目。\r\n */\r\nexport function collectQaFromNodes(snapshot: unknown): RailMessage[] {\r\n const out: RailMessage[] = []\r\n for (const node of nodeValuesOf(snapshot)) {\r\n if (node === null || typeof node !== 'object') continue\r\n const n = node as { kind?: unknown, anchorSeq?: unknown, data?: unknown }\r\n // `tool-call` 是**独立节点**(不是助手消息里的块):\r\n // ui-chat/conversation-nodes/tool.ts 的 toolDefinition.kind = 'tool-call',\r\n // data 形如 { root }(ToolChatData)。\r\n if (n.kind !== 'tool-call') continue\r\n const root = (n.data as { root?: unknown } | undefined)?.root\r\n if (root === null || typeof root !== 'object') continue\r\n const r = root as {\r\n callId?: unknown\r\n name?: unknown\r\n argsRaw?: unknown\r\n call?: { name?: unknown, argsRaw?: unknown }\r\n seq?: unknown\r\n time?: unknown\r\n }\r\n const callId = typeof r.callId === 'string' ? r.callId : ''\r\n if (callId === '') continue\r\n // 运行中的根调用把 name/argsRaw 放在 root 上;完成后落在 root.call 里\r\n const name = typeof r.name === 'string' ? r.name : (typeof r.call?.name === 'string' ? r.call.name : '')\r\n if (name !== 'ask_user_question') continue\r\n const argsRaw = typeof r.argsRaw === 'string' ? r.argsRaw : (typeof r.call?.argsRaw === 'string' ? r.call.argsRaw : '')\r\n const asked = questionsTextOf(argsRaw)\r\n if (asked === '') continue\r\n out.push({\r\n seq: typeof n.anchorSeq === 'number' ? n.anchorSeq : 0,\r\n time: typeof r.time === 'number' ? r.time : 0,\r\n text: asked,\r\n hasImage: false,\r\n // 两个身份分开:node key 用于「是否已加载」,DOM 锚点用于定位行。\r\n key: typeof (n as { key?: unknown }).key === 'string' ? (n as { key: string }).key : undefined,\r\n // 官方工具调用的 DOM 锚点格式(ui-tool/ToolCallTree.tsx:36)\r\n anchor: `call:${callId}`,\r\n qa: true,\r\n })\r\n }\r\n out.sort((a, b) => a.seq - b.seq)\r\n return out\r\n}\r\n\r\n/**\r\n * 属于第 `index` 条消息区间的问答——**只归给它前面最近的那条用户消息**。\r\n *\r\n * 区间取 `[本条 seq, 下一条消息的 seq)`,最后一条消息无上界。\r\n * 判据不能只写 `qa.seq >= m.seq`:那会让同一条提问落进**所有更早消息**的 tip\r\n * (2026-09-14 用户报告的归属 bug)。依据是「提问一定在某条消息的回答中」——\r\n * 它必然夹在两条用户消息之间,因此上界就是下一条消息。\r\n *\r\n * @param qaMessages - 已按 seq 升序的问答条目(collectQaFromNodes 的产出)。\r\n * @param messages - 完整的用户消息列表(**不是**收藏过滤后的列表:区间按消息序列划分)。\r\n * @param index - 目标消息在 `messages` 中的下标。\r\n * @returns 落在该区间内的问答条目(保持输入顺序)。\r\n */\r\nexport function qaWithinMessage(\r\n qaMessages: readonly RailMessage[],\r\n messages: readonly RailMessage[],\r\n index: number,\r\n): RailMessage[] {\r\n const self = messages[index]\r\n if (self === undefined) return []\r\n const next = messages[index + 1]\r\n const upper = next === undefined ? undefined : next.seq\r\n return qaMessages.filter((q) => q.seq >= self.seq && (upper === undefined || q.seq < upper))\r\n}\r\n\r\n/** 问答条目的预览文本:取 `arguments` JSON 里的问题标题(多问用 ` / ` 连)。 */\r\nfunction questionsTextOf(argsRaw: unknown): string {\r\n if (typeof argsRaw !== 'string' || argsRaw.trim() === '') return ''\r\n let parsed: unknown\r\n try { parsed = JSON.parse(argsRaw) } catch { return '' }\r\n const list = (parsed as { questions?: unknown } | null)?.questions\r\n if (!Array.isArray(list)) return ''\r\n const parts: string[] = []\r\n for (const q of list) {\r\n if (q === null || typeof q !== 'object') continue\r\n const o = q as { header?: unknown, question?: unknown }\r\n const text = typeof o.question === 'string' && o.question !== ''\r\n ? o.question\r\n : typeof o.header === 'string' ? o.header : ''\r\n if (text !== '') parts.push(text)\r\n }\r\n return parts.join(' / ').trim().slice(0, 80)\r\n}\r\n\r\n/** Resolve the DOM anchor key (`data-chat-anchor-key`) for one rail message. */\r\nfunction anchorKeyOf(m: RailMessage): string | undefined {\r\n // 问答条目把两个身份分开存:node key 在 m.key,DOM 锚点在 m.anchor\r\n if (typeof m.anchor === 'string' && m.anchor !== '') return m.anchor\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/** Resolve the **chat node key** for one rail message (used for load checks). */\r\nfunction nodeKeyOf(m: RailMessage): string | undefined {\r\n return typeof m.key === 'string' && m.key !== '' ? m.key : anchorKeyOf(m)\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/** Resolve on the next animation frame, or immediately where rAF is absent. */\r\nconst nextFrame = (): Promise<void> => new Promise<void>((resolve) => {\r\n if (typeof requestAnimationFrame !== 'function') { resolve(); return }\r\n requestAnimationFrame(() => { resolve() })\r\n})\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 the landing is about to run (indicator goes away before the single\r\n * re-verify — 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: the landing is the visible feedback — drop the busy indicator\r\n // here even though the single re-verify step still runs.\r\n onJump?.('landed')\r\n // DSH 0.1.2-alpha.3+ paging compensates the reader position while the newly\r\n // prepended window re-flows — a scroll issued right after a multi-page\r\n // loadThrough lands mid-flight and gets overridden by the compensation\r\n // (verified on the alpha.4 kernel: 145971 → 44464 snap-back). Landing waits\r\n // one frame for the prepended layout to settle (the official jump schedules\r\n // its reconcile on requestAnimationFrame the same way), then re-verifies once:\r\n // a stray bottom-follow can still pull the landing 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 // 瞬时定位,与官方 TurnNavigator 一致:`use-chat-viewport.ts` 的 `write()`\r\n // 就是直接赋 `scroller.scrollTop`。平滑滚动在长跨度下要跨越整段已加载窗口,\r\n // 窗口失焦时还可能完全不推进——反而比瞬时更不可靠。\r\n scrollport.scrollTop = target\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 await nextFrame()\r\n if (signal?.aborted) return false\r\n scrollToRow()\r\n await delay(600)\r\n if (signal?.aborted) return false\r\n // 单次复核:分页补偿若仍在进行,会把刚落的位置拉回去(见上)。\r\n if (rowDelta() > Math.max(120, scrollport.getBoundingClientRect().height * 0.2)) scrollToRow()\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 // 「提问&回答」**不进导航条**(2026-09-14 用户重新设计):\r\n // 它挂在所属消息的 hover tip 面板里,在 tip 中点击跳转到问答本身。\r\n // 这样导航条只保留一级用户消息,完全规避了「二级条目要缩进 + 一级/二级指示线\r\n // 位置一样无法区分」的层级问题。\r\n const qaMessages = collectQaFromNodes(nodeSnapshot)\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 railSettingsForm !== null ? (cb: () => void) => railSettingsForm!.subscribe(cb) : NOOP_SUB,\r\n () => railSettingsForm?.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 // ── 会话区剩余空间 → 空间不足时隐藏(2026-09-14 用户要求)──────────────────\r\n // 官方右侧边栏在宽屏是「挤压会话区」,窄屏会**完全遮住**会话区。遮住之后这条\r\n // 导航条失去参考物,应该跟着退场(用户原话:「会话区域都被遮挡时,消息导航条\r\n // 也就没有存在意义了,应该也不展示」)。\r\n //\r\n // 定位与显隐都锚在**会话列的右缘**上:官方右侧边栏默认 push 模式(面板展开时\r\n // 会话区让出空间),此时会话列右缘左移,rail 必须跟着走;而 `--dsh-sidebar-width`\r\n // 在当前内核恒为 0px,读它等于把 rail 钉死在视口右缘(右侧边栏一开就被压住)。\r\n //\r\n // 显隐判据(用户拍板):**会话区剩余宽度不足就隐藏**——会话列窄到没有参考价值\r\n // 时退场,右侧边栏造成的挤压同样计入。\r\n const [anchor, setAnchor] = useState<{ right: number, width: number } | null>(null)\r\n useEffect(() => {\r\n const measure = (): void => {\r\n if (typeof document === 'undefined') return\r\n const scrollport = document.querySelector<HTMLElement>('[data-conversation-scroll]')\r\n if (scrollport === null) { setAnchor(null); return }\r\n const rect = scrollport.getBoundingClientRect()\r\n if (rect.width === 0) { setAnchor(null); return }\r\n const next = { right: Math.max(0, Math.round(window.innerWidth - rect.right)), width: Math.round(rect.width) }\r\n // 值不变时沿用旧对象,避免每轮采样都重渲染\r\n setAnchor((prev) => (prev !== null && prev.right === next.right && prev.width === next.width ? prev : next))\r\n }\r\n measure()\r\n const scrollport = document.querySelector<HTMLElement>('[data-conversation-scroll]')\r\n const observer = typeof ResizeObserver === 'undefined' || scrollport === null ? null : new ResizeObserver(measure)\r\n if (observer !== null && scrollport !== null) observer.observe(scrollport)\r\n window.addEventListener('resize', measure)\r\n // 兜底采样:右侧边栏开合改的是它自己的轨道宽度,过渡约 300ms\r\n const timer = window.setInterval(measure, 400)\r\n return () => {\r\n if (observer !== null) observer.disconnect()\r\n window.removeEventListener('resize', measure)\r\n window.clearInterval(timer)\r\n }\r\n }, [sessionId])\r\n const spaceTight = anchor !== null && anchor.width < RAIL_MIN_SPACE\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)\r\n const listRef = 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 // rail 与 tip 之间有间隙:指针从条目移向 tip 时会先触发 item 的 mouseleave。\r\n // 留一点宽限(180ms),并在指针真正进入 tip 时取消,否则 tip 会在指针到达前\r\n // 消失、其中的问答按钮点不到(实测坐标点击无效而元素 .click() 有效即此因)。\r\n const tipCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\r\n const cancelTipClose = () => {\r\n if (tipCloseTimerRef.current !== null) {\r\n clearTimeout(tipCloseTimerRef.current)\r\n tipCloseTimerRef.current = null\r\n }\r\n }\r\n const handleItemLeave = (index: number) => {\r\n cancelTipClose()\r\n tipCloseTimerRef.current = setTimeout(() => {\r\n tipCloseTimerRef.current = null\r\n setTip((prev) => (prev?.index === index ? null : prev))\r\n }, 180)\r\n }\r\n useEffect(() => cancelTipClose, [])\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 // 用户消息锚点是 `13:input-message<id>`;问答条目(②)挂在官方工具调用锚点上,\r\n // 前缀是 `call:`。两者都要扫,否则问答永远算不出「当前位置」。\r\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"],[data-chat-anchor-key^=\"call:\"]')\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 // 滚动的是内层列表容器(rail 自身不滚动,header 不参与滚动)\r\n const el = listRef.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 // 可寻址钩子:挂上真实锚点与问答标记,便于定位/排查(也让实机验证能直接断言)\r\n ...(key !== undefined ? { 'data-crl-anchor': key } : {}),\r\n ...(m.qa === true ? { 'data-crl-qa': 'true' } : {}),\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 // 判断「是否已加载」要用 **node key**:问答条目的 key 是 tool-call 节点键,\r\n // 不是 DOM 锚点(混用会永远判定未加载 → 不滚动)。\r\n nodeKeyOf(m) as string,\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 — the landing itself is\r\n // the visible feedback.\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}` : '') + (spaceTight ? ` ${NAV_HIDDEN_CLASS}` : ''),\r\n // 右缘锚在会话列上:右侧边栏 push 时会话列左移,rail 随之左移(transition 带动画)\r\n ...(anchor !== null ? { style: { right: `${anchor.right + 3}px` } } : {}),\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 // 条目放进独立滚动容器:header(收藏开关)在流内、不再压在条目上\r\n // (sticky header 会盖住滚到它下面的条目——用户截图反馈的「遮挡」)。\r\n createElement('div', { ref: listRef, key: 'list', className: S.list }, ...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 // 指针进入 tip 时取消待关闭定时器(见 handleItemLeave 的宽限说明)\r\n onMouseEnter: () => cancelTipClose(),\r\n onMouseLeave: () => setTip(null),\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 // 只收「本条消息之后、下一条消息之前」的问答——归给前面最近的那条消息。\r\n // 区间用完整 messages(而非收藏过滤后的列表)划分:归属由消息序列决定。\r\n const qaForTip = qaWithinMessage(qaMessages, messages, tipIndex)\r\n if (qaForTip.length > 0) {\r\n children.push(createElement('div', { className: S.tipQaWrap, key: 'qa' },\r\n qaForTip.map((q) => createElement('button', {\r\n type: 'button',\r\n key: q.key ?? q.anchor,\r\n className: S.tipQa,\r\n title: q.text,\r\n onClick: (e: MouseEvent) => {\r\n e.stopPropagation()\r\n if (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 nodeKeyOf(q) as string,\r\n (k) => chatNodeOf(fallbackStore.getSnapshot(), k),\r\n undefined,\r\n controller.signal,\r\n q.seq,\r\n (phase) => setJumping(phase === 'paging'),\r\n ).finally(() => setJumping(false))\r\n },\r\n children: [\r\n // Lucide `circle-help` at a 16-unit grid: outline icon, never a\r\n // rendered glyph (the boxed \"?\" read as an emoji marker).\r\n createElement('svg', {\r\n key: 'icon',\r\n className: S.tipQaIcon,\r\n viewBox: '0 0 16 16',\r\n width: 13,\r\n height: 13,\r\n fill: 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 1.6,\r\n strokeLinecap: 'round',\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n },\r\n createElement('circle', { cx: 8, cy: 8, r: 6.5, 'aria-hidden': true }),\r\n createElement('path', { d: 'M6.06 6a2 2 0 0 1 3.89.67c0 1.33-2 2-2 2', 'aria-hidden': true }),\r\n createElement('path', { d: 'M8 11.33h.01', 'aria-hidden': true })),\r\n createElement('span', { className: S.tipQaText, key: 'txt' }, q.text),\r\n ],\r\n }))))\r\n }\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 // 设置(Plugins 页):0.1.7 的槽是 `plugins.bundle.config`,key 用**包名**。\r\n // 判据(`ui-plugin-manager/src/client/config-ledger.ts`):`plugins.bundle.config` 的\r\n // key 直接取 `entry.options.key`,而 `plugins.row.config` 才用 `包名#行id`\r\n // (`rowConfigKey`);本插件 package.json 声明了 `dsh.bundle.patch`,自身即 bundle。\r\n // 旧的 `settings.plugin.item` 在 0.1.7 client 包里只剩一条注释;替代者 `plugins.item`\r\n // 按契约是官方插件专区(\"OCCUPIED by the official settings pages\"),第三方不该占。\r\n const settingsForm = ctx.configForms.get<RailSettings>('chat-rail')\r\n bindRailSettingsForm(settingsForm)\r\n ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({\r\n name: 'plugins.bundle.config',\r\n key: '@max-null/dsh-chat-rail',\r\n inject: () => ({\r\n setShowOfficialNavigator: (show: boolean) => railSettingsForm?.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 railSettingsForm !== null ? (cb: () => void) => railSettingsForm!.subscribe(cb) : NOOP_SUB,\r\n () => railSettingsForm?.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. POSITION ANCHOR. The rail's right edge follows the conversation column\r\n * (`[data-conversation-scroll]`), not the viewport: the official right\r\n * sidebar pushes the conversation narrower, and the rail must travel with\r\n * it. `--dsh-sidebar-width` reads 0px on the current kernel, so a CSS-only\r\n * offset would pin the rail underneath that sidebar.\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\n// 0.1.7:`@deepseek-ai/dsh-client-runtime` 已被移除(`packages/client` 下再无 runtime\r\n// 目录,npm 上停更于 0.1.1-rc.2)。三种类型各归其位——这也是官方 client 包的统一写法\r\n// (`ui-conversation`、`ui-commands`、`ui-session` 等 163 处均如此):\r\n// · ClientContext → cordis 的 Context 别名\r\n// · SessionId → dsh-session/types(`SessionId = Branded<'SessionId'>`)\r\n// · ISessions → dsh-api-session-controller/client(契约在 client/contract/sessions.ts)\r\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\r\nimport type { SessionId } from '@deepseek-ai/dsh-session/types'\r\nimport type { ISessions } from '@deepseek-ai/dsh-api-session-controller/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// Type-only:Context / SlotMap 增强声明。把 ClientContext 还原成裸 `Context` 之后,\r\n// `ctx.slots`、`ctx.configForms` 以及 `plugins.bundle.config` 的 SlotMap 条目都要靠\r\n// 这些包的 declare module 才能存在(它们不产生运行时代码)。\r\n// `ctx.slots` 的合并来自 **renderer 包**(slot 注册表的属主),不是 ui-slots——\r\n// 后者是 shell 预加载的静态库,没有可安装的 `./client` 入口。\r\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\r\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\r\nimport type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'\r\n\r\nexport const inject = ['slots', 'sessions', 'conversation', 'configForms']\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 由组件按**会话列右缘**计算后用内联样式给出(见 anchor);这里的 3px 只是\r\n// 会话列尚未就绪时的兜底。`--dsh-sidebar-width` 在当前内核恒为 0px,不能用它定位——\r\n// 那会把 rail 钉死在视口右缘,右侧边栏一开就被压在面板下面。\r\nconst css = [\r\n // 展开/收起用 ease-in-out 且两侧对称时长,减少「抖一下」(原来 width 用\r\n // cubic-bezier(.4,0,.2,1)、right/top 用 var(--ds-transition-duration-slow),\r\n // 两条曲线不同步,中间过程会互相追)。\r\n // z-index 5:官方 frame 的浮层梯度(AppFrame.module.css)是「右侧面板 10 → 拖拽手柄 11\r\n // → 标题栏座位 15 → shell.overlay 20 → 全屏右面板 40」。rail 是常驻导航,只该压在\r\n // 会话列内容(0)之上,不该进浮层那一档。\r\n // · 100 与 Menu/tooltip 同层,rail 是 body 末尾 portal,同层时 DOM 靠后者在上 → 盖住菜单\r\n // (2026-09-29 实机)。\r\n // · 90 只调开了与菜单的先后,仍高于**整层 shell.overlay(20)** —— 插件中心的「插件更新」\r\n // 弹窗注册在那层里且自身没有 portal,照样被 rail 挡住(2026-09-30 用户截图)。\r\n // 低于 20 才是结构性修法:那一层里的一切(无论有没有自己的 z-index)都赢 rail。\r\n '.crl_nav{user-select:none;z-index:5;position:fixed;right:3px;top:calc((100vh - var(--dsh-sidebar-height,0px)) / 2);transform:translateY(-50%);width:36px;max-height:min(60vh,420px,calc(100vh - var(--dsh-sidebar-height,0px) - 40px));display:flex;flex-direction:column;align-items:center;box-sizing:border-box;padding:0 0 10px;border-radius:18px;overflow-y:hidden;overflow-x:hidden;background:rgba(255,255,255,.55);border:1px solid rgba(0,0,0,.07);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);scrollbar-width:none;transition:width .25s cubic-bezier(.4,0,.2,1),right .25s cubic-bezier(.4,0,.2,1),top .25s cubic-bezier(.4,0,.2,1),background .2s ease,border-color .2s ease,box-shadow .2s ease}',\r\n // 列表容器:rail 自身不再滚动,滚动只发生在这一层,header 因此永远不覆盖条目。\r\n '.crl_list{display:flex;flex-direction:column;align-items:center;width:100%;min-height:0;flex:1 1 auto;position:relative;overflow-y:hidden;overflow-x:hidden;scrollbar-width:none}',\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;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,.crl_list::-webkit-scrollbar{width:0;height:0}',\r\n // 会话区 Y 轴滚动条:官方内核 0.1.5 起把 bar 从「紧贴内容边缘」改为「离边缘 2px」\r\n // (`ConversationRoot.module.css` 的 `margin-right: 2px` + track 内缩),于是一条\r\n // 独立细条落在导航条旁边——与导航条的位置指示功能重复,视觉上是噪声,由本插件\r\n // 统一隐藏(滚轮/触控板滚动不受影响)。\r\n //\r\n // 做法是**把 thumb 画成透明**,而不是把滚动条宽度压成 0:宽度归零会连带回收\r\n // `scrollbar-gutter: stable` 的 8px 槽位,内容区因此变宽、输入卡横移,而官方\r\n // 无条件保留槽位正是为了跨视图时输入卡不位移(`ConversationRoot.module.css` 的\r\n // gutter 注释 + `2026-08-04-composer-tab-gutter-reservation`)。track 官方本来就\r\n // 透明,所以只需这一条;thumb:hover 一并列出,避免特异性与注入顺序决定 hover 态。\r\n //\r\n // 两条边界:① **不写 `scrollbar-width`** —— ui-theme/scrollbar.css 注释记录了实测,\r\n // 非 auto 的 `scrollbar-width` 会让 Chromium 丢弃该元素的全部 `::-webkit-scrollbar*`\r\n // 规则;② 用官方契约锚点 `[data-conversation-scroll]`,不依赖 CSS module hash。\r\n '[data-conversation-scroll]::-webkit-scrollbar-thumb,[data-conversation-scroll]::-webkit-scrollbar-thumb:hover{background:transparent}',\r\n // 列表独立滚动:header(收藏开关)留在流内、不覆盖列表内容。\r\n '.crl_show .crl_list{overflow-y:auto;align-items:stretch}',\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 // 折叠态**居中**:指示线(宽 10px)与上方星标按钮(宽 26px)都以容器中线为轴,\r\n // 二者中心同在一列。此前是 flex-end,指示线被推到右边(中心≈22px)而星标居中\r\n // (18px),用户截图指出的「星和导航条没对齐」即此。\r\n '.crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:center;align-items:center;width:100%;box-sizing:border-box;padding:0 9px;line-height:20px;background:none;border:none;font:inherit;text-align:left;color:rgba(0,0,0,.68);transition:padding .25s cubic-bezier(.4,0,.2,1),color .15s ease}',\r\n // 展开态回到 flex-end:此时要的是「#序号 标题 时间 [指示线]」右端对齐,居中会让文字行变得难读。\r\n '.crl_show .crl_item{justify-content:flex-end;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 // tip 里现在有**可点击项**(提问&回答),所以容器不能再 `pointer-events:none`;\r\n // 同时需要一点存活宽限:鼠标从 rail 移到 tip 的途中会先触发 item 的 mouseleave,\r\n // 否则 tip 在指针到达之前就被关掉,里面的按钮根本点不到(实测:坐标点击无效、\r\n // 而元素 .click() 有效,即为此竞态)。\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:auto}',\r\n // tip 里的「提问&回答」区(用户 2026-09-14 重新设计:问答不进导航条,收在这里)\r\n '.crl_tipQaWrap{margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));display:flex;flex-direction:column;gap:4px;pointer-events:auto}',\r\n '.crl_tipQa{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;padding:5px 8px;border:none;border-radius:7px;background:rgba(77,107,254,.08);color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background .15s ease}',\r\n '.crl_tipQa:hover{background:rgba(77,107,254,.16)}',\r\n '.crl_tipQaIcon{flex:none;display:block;color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n '.crl_tipQaText{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',\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 header action: 「只显示收藏」开关。折叠态是一个圆图标,展开态是整行的\r\n // header(星标 + 文案 + 底部一条分隔线),与下方消息列表分层。\r\n //\r\n // 历史坑(2026-09-14 用户截图反馈):这里曾用 `position:sticky; top:-10px`\r\n // 把按钮上推到胶囊**外面**,代价是圆形 hover 背景骑在面板上边缘之上,看起来\r\n // 像浮在导航条外面。sticky 本身是对的(展开态 `.crl_nav.crl_show` 是\r\n // `overflow-y:auto`,列表会滚,header 必须钉住),错的是那 −10px。\r\n //\r\n // 正确解法:`top:0` 与胶囊内边缘齐平,并给按钮**自己的不透明底色**——滚动行\r\n // 从容器 padding 缝隙经过时被底色挡住,而不是靠把控件挪出容器。展开态再补一条\r\n // 底部分隔线,读起来就是 panel header。\r\n // 折叠态:**居中**,与下方已居中的指示线同轴(此前 align-self:flex-end 是为了\r\n // 迁就 flex-end 的指示线;指示线改居中后,这里也回居中)。\r\n // transition 里必须带 margin/left:rail 宽度在 36px↔280px 之间变化时,居中的\r\n // 按钮靠 margin 自动重算来跟随,加过渡后才是「滑过去」而不是「闪过去」\r\n // (用户明确提出的观感问题)。\r\n // 折叠态与展开态同构:收藏开关都是面板的 header(通栏 + 底部分界线),命令式地\r\n // 去掉原先那块白色圆角底——它在半透明胶囊里是一块突兀的白斑,与展开态的通栏\r\n // 标题栏不是一套语言。折叠态保留顶部 10px 内边距,避开胶囊的半圆头。\r\n '.crl_favToggle{flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:6px;width:34px;height:26px;margin:0 0 6px;padding:0;border:none;border-radius:0;background:transparent;border-bottom:1px solid rgba(0,0,0,.07);color:rgba(0,0,0,.42);cursor:pointer;font-size:11px;line-height:1;white-space:nowrap;transition:background .15s ease,color .15s ease,margin .25s cubic-bezier(.4,0,.2,1),left .25s cubic-bezier(.4,0,.2,1)}',\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 // 展开态:整行 header(宽度与胶囊内宽一致),底部分隔线把它和消息行分开。\r\n // 折叠/展开都用「居中 + 对称边距」,这样宽度变化时位置连续、不跳变。\r\n // 展开态:收藏开关作为通栏标题栏贴到面板顶部——上方不再留内边距与自成的边框,\r\n // 分界由它自己的底边框承担;顶部两角交给 rail 的圆角裁切。\r\n '.crl_show .crl_favToggle{width:100%;height:28px;margin:0 0 6px;padding:0 12px;border-radius:0;justify-content:flex-start;text-align:left;border-bottom:1px solid rgba(0,0,0,.07)}',\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);border-bottom-color:rgba(255,255,255,.1)}',\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_show .crl_favToggle,[data-theme=\\'dark\\'] .crl_show .crl_favToggle,.dark .crl_show .crl_favToggle{border-bottom-color:rgba(255,255,255,.1)}',\r\n // 空间不足(会话区被右侧边栏遮到没有参考价值)时整体退场。\r\n // 用 opacity + pointer-events 而不是 display:none —— 官方模式的切换要能直接\r\n // 复用 nav 的内联 display,两者不该互相覆盖。\r\n // 选择器带 .crl_nav 提特异性:单类版本实测只有 pointer-events 生效、opacity 未淡出。\r\n '.crl_nav.crl_navHidden{opacity:0 !important;pointer-events:none !important}',\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/**\r\n * `data-plugin` 归属标记:DSH 的 client 模块系统只把**没有** `data-plugin` 的 `<style>`\r\n * 认领给正在 materialize 的模块(`claimStyles`),而 HMR 重载时按\r\n * `data-plugin === 模块 id` 逐字匹配删除。裸注入的 style 会被任意模块认领走,\r\n * 随后随那个模块的一次重载被物理删除——元素还在、样式消失(悬浮球样式事故的机制)。\r\n * 这里用一个独立于模块 id 的稳定值:既不进别人的账,也不随本模块的进出被清掉。\r\n */\r\nconst HIDE_OFFICIAL_PLUGIN_TAG = 'dsh-chat-rail-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.setAttribute('data-plugin', HIDE_OFFICIAL_PLUGIN_TAG)\r\n tag.textContent = HIDE_OFFICIAL_CSS\r\n document.head.appendChild(tag)\r\n } else if (el.textContent !== HIDE_OFFICIAL_CSS) {\r\n // 同主样式:内容比对,避免 HMR 重载后旧内容占住标签\r\n el.textContent = HIDE_OFFICIAL_CSS\r\n }\r\n}\r\n\r\n/**\r\n * 会话区剩余宽度低于此值时隐藏导航条(px)。\r\n *\r\n * 36px 是 rail 自身宽度,留 ~220px 给消息区——再窄时指示点已无参考意义。\r\n */\r\nconst RAIL_MIN_SPACE = 260\r\n\r\n/** 导航条隐藏类(空间不足时由 React 加上;用类而非内联 display,避免与官方模式冲突)。 */\r\nconst NAV_HIDDEN_CLASS = 'crl_navHidden'\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// 配置表单(0.1.7 起以 `ctx.configForms.get(entryId)` 取代 settingsScope 服务)。\r\ntype RailSettings = { showOfficialNavigator?: boolean }\r\nlet railSettingsForm: {\r\n getSnapshot(): { value?: RailSettings }\r\n subscribe(listener: () => void): () => void\r\n set(key: 'showOfficialNavigator', value: boolean): Promise<unknown>\r\n} | null = null\r\n\r\n/**\r\n * client apply 时绑定(由 `ctx.configForms.get('chat-rail')` 取得)。\r\n *\r\n * 运行时面与旧的 `settingsScope.bind()` 同形:`ConfigForm<T>` 提供 `getSnapshot` /\r\n * `subscribe` / `set(field, value)`(`ui-settings/src/client/config-form-types.ts:39`),\r\n * 所以两处订阅与写入的调用点都不用改。模块级句柄是既有骨架,本次只换来源、不动结构。\r\n * @param form - Host 侧 `configForms` 服务为 `chat-rail` 这个 entry 提供的表单。\r\n */\r\nexport function bindRailSettingsForm(form: unknown): void {\r\n if (typeof form !== 'object' || form === null) return\r\n railSettingsForm = form as typeof railSettingsForm\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\n// 内容比对而非「已存在就跳过」:HMR 重载会重新执行本模块,若沿用旧标签,旧 CSS 会\r\n// 一直占住这个 id,改动看着像\"HMR 生效了\"却仍是旧规则(实测:只能重启内核才看到)。\r\nif (typeof document !== 'undefined') {\r\n const existing = document.head.querySelector<HTMLStyleElement>(`style[data-plugin-css=\"${STYLE_ID}\"]`)\r\n if (existing === 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 } else if (existing.textContent !== css) {\r\n existing.textContent = css\r\n }\r\n}\r\n\r\nconst S = {\r\n nav: 'crl_nav',\r\n navShow: 'crl_show',\r\n list: 'crl_list',\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 tipQaWrap: 'crl_tipQaWrap',\r\n tipQa: 'crl_tipQa',\r\n tipQaIcon: 'crl_tipQaIcon',\r\n tipQaText: 'crl_tipQaText',\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\nexport interface 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 * DOM 锚点键(`data-chat-anchor-key` 的值),与 `key`(chat node key)**不同**。\r\n *\r\n * 问答条目由 `collectQaFromNodes` 设置:它的 `key` 是 tool-call 的 **node key**\r\n * (jumpToMessage 用它判断\"是否已加载\"),而锚点是 `call:<callId>`\r\n * (ui-tool/ToolCallTree.tsx:36 设的那个属性,用来在 DOM 里定位行)。\r\n * 两者混用会导致「永远判定未加载 → 不滚动」。\r\n */\r\n anchor?: string\r\n /**\r\n * 「提问&回答」条目(`ask_user_question` 的 tool-call)。\r\n *\r\n * 它们由 `collectQaFromNodes` 从助手节点收集,与批量投影无关;`key` 是官方\r\n * 工具调用的 DOM 锚点 `call:<callId>`,因此同样能跳转。\r\n */\r\n qa?: boolean\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/** 任一数据平面的节点集合(与 collectFromNodes 同一套取值规则)。 */\r\nfunction nodeValuesOf(snapshot: unknown): unknown[] {\r\n const store = (snapshot as ChatSnapshotLike | undefined)?.nodes as ChatNodeStoreLike | undefined\r\n if (store !== undefined && typeof store.values === 'function') {\r\n return [...store.values()]\r\n }\r\n const chat = (snapshot as { chat?: { nodes?: Map<unknown, unknown> } } | undefined)?.chat\r\n if (chat?.nodes === undefined) return []\r\n return [...chat.nodes.values()]\r\n}\r\n\r\n/**\r\n * 「提问&回答」也要能被导航。\r\n *\r\n * 它**不是** chat node:`ask_user_question` 是助手消息里的一个 tool-call 块,\r\n * 用户的答案落在同一个 callId 的 `tool/result`(实测会话日志:有 `tool/call\r\n * name=ask_user_question` 与对应结果,**没有任何带问答痕迹的 user/message**)。\r\n * 所以 `railMessageOfNode` 的 `kind==='user'` 判定永远覆盖不到它。\r\n *\r\n * 但官方给工具调用也设了 DOM 锚点(`ui-tool/ToolCallTree.tsx:36` →\r\n * `data-chat-anchor-key={`call:${callId}`}`),所以**可跳转**——只要把\r\n * 助手节点里的问答块收集成 rail 条目即可。\r\n *\r\n * 范围:只覆盖已加载的 chat node(不做 loadThrough 拉取未加载历史)——\r\n * 用户 2026-09-14 拍板。\r\n *\r\n * @param snapshot - 当前 chat node 快照(任一平面)。\r\n * @returns 按 seq 升序的问答条目。\r\n */\r\nexport function collectQaFromNodes(snapshot: unknown): RailMessage[] {\r\n const out: RailMessage[] = []\r\n for (const node of nodeValuesOf(snapshot)) {\r\n if (node === null || typeof node !== 'object') continue\r\n const n = node as { kind?: unknown, anchorSeq?: unknown, data?: unknown }\r\n // `tool-call` 是**独立节点**(不是助手消息里的块):\r\n // ui-chat/conversation-nodes/tool.ts 的 toolDefinition.kind = 'tool-call',\r\n // data 形如 { root }(ToolChatData)。\r\n if (n.kind !== 'tool-call') continue\r\n const root = (n.data as { root?: unknown } | undefined)?.root\r\n if (root === null || typeof root !== 'object') continue\r\n const r = root as {\r\n callId?: unknown\r\n name?: unknown\r\n argsRaw?: unknown\r\n call?: { name?: unknown, argsRaw?: unknown }\r\n seq?: unknown\r\n time?: unknown\r\n }\r\n const callId = typeof r.callId === 'string' ? r.callId : ''\r\n if (callId === '') continue\r\n // 运行中的根调用把 name/argsRaw 放在 root 上;完成后落在 root.call 里\r\n const name = typeof r.name === 'string' ? r.name : (typeof r.call?.name === 'string' ? r.call.name : '')\r\n if (name !== 'ask_user_question') continue\r\n const argsRaw = typeof r.argsRaw === 'string' ? r.argsRaw : (typeof r.call?.argsRaw === 'string' ? r.call.argsRaw : '')\r\n const asked = questionsTextOf(argsRaw)\r\n if (asked === '') continue\r\n out.push({\r\n seq: typeof n.anchorSeq === 'number' ? n.anchorSeq : 0,\r\n time: typeof r.time === 'number' ? r.time : 0,\r\n text: asked,\r\n hasImage: false,\r\n // 两个身份分开:node key 用于「是否已加载」,DOM 锚点用于定位行。\r\n key: typeof (n as { key?: unknown }).key === 'string' ? (n as { key: string }).key : undefined,\r\n // 官方工具调用的 DOM 锚点格式(ui-tool/ToolCallTree.tsx:36)\r\n anchor: `call:${callId}`,\r\n qa: true,\r\n })\r\n }\r\n out.sort((a, b) => a.seq - b.seq)\r\n return out\r\n}\r\n\r\n/**\r\n * 属于第 `index` 条消息区间的问答——**只归给它前面最近的那条用户消息**。\r\n *\r\n * 区间取 `[本条 seq, 下一条消息的 seq)`,最后一条消息无上界。\r\n * 判据不能只写 `qa.seq >= m.seq`:那会让同一条提问落进**所有更早消息**的 tip\r\n * (2026-09-14 用户报告的归属 bug)。依据是「提问一定在某条消息的回答中」——\r\n * 它必然夹在两条用户消息之间,因此上界就是下一条消息。\r\n *\r\n * @param qaMessages - 已按 seq 升序的问答条目(collectQaFromNodes 的产出)。\r\n * @param messages - 完整的用户消息列表(**不是**收藏过滤后的列表:区间按消息序列划分)。\r\n * @param index - 目标消息在 `messages` 中的下标。\r\n * @returns 落在该区间内的问答条目(保持输入顺序)。\r\n */\r\nexport function qaWithinMessage(\r\n qaMessages: readonly RailMessage[],\r\n messages: readonly RailMessage[],\r\n index: number,\r\n): RailMessage[] {\r\n const self = messages[index]\r\n if (self === undefined) return []\r\n const next = messages[index + 1]\r\n const upper = next === undefined ? undefined : next.seq\r\n return qaMessages.filter((q) => q.seq >= self.seq && (upper === undefined || q.seq < upper))\r\n}\r\n\r\n/** 问答条目的预览文本:取 `arguments` JSON 里的问题标题(多问用 ` / ` 连)。 */\r\nfunction questionsTextOf(argsRaw: unknown): string {\r\n if (typeof argsRaw !== 'string' || argsRaw.trim() === '') return ''\r\n let parsed: unknown\r\n try { parsed = JSON.parse(argsRaw) } catch { return '' }\r\n const list = (parsed as { questions?: unknown } | null)?.questions\r\n if (!Array.isArray(list)) return ''\r\n const parts: string[] = []\r\n for (const q of list) {\r\n if (q === null || typeof q !== 'object') continue\r\n const o = q as { header?: unknown, question?: unknown }\r\n const text = typeof o.question === 'string' && o.question !== ''\r\n ? o.question\r\n : typeof o.header === 'string' ? o.header : ''\r\n if (text !== '') parts.push(text)\r\n }\r\n return parts.join(' / ').trim().slice(0, 80)\r\n}\r\n\r\n/** Resolve the DOM anchor key (`data-chat-anchor-key`) for one rail message. */\r\nfunction anchorKeyOf(m: RailMessage): string | undefined {\r\n // 问答条目把两个身份分开存:node key 在 m.key,DOM 锚点在 m.anchor\r\n if (typeof m.anchor === 'string' && m.anchor !== '') return m.anchor\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/** Resolve the **chat node key** for one rail message (used for load checks). */\r\nfunction nodeKeyOf(m: RailMessage): string | undefined {\r\n return typeof m.key === 'string' && m.key !== '' ? m.key : anchorKeyOf(m)\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/** Resolve on the next animation frame, or immediately where rAF is absent. */\r\nconst nextFrame = (): Promise<void> => new Promise<void>((resolve) => {\r\n if (typeof requestAnimationFrame !== 'function') { resolve(); return }\r\n requestAnimationFrame(() => { resolve() })\r\n})\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 the landing is about to run (indicator goes away before the single\r\n * re-verify — 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: the landing is the visible feedback — drop the busy indicator\r\n // here even though the single re-verify step still runs.\r\n onJump?.('landed')\r\n // DSH 0.1.2-alpha.3+ paging compensates the reader position while the newly\r\n // prepended window re-flows — a scroll issued right after a multi-page\r\n // loadThrough lands mid-flight and gets overridden by the compensation\r\n // (verified on the alpha.4 kernel: 145971 → 44464 snap-back). Landing waits\r\n // one frame for the prepended layout to settle (the official jump schedules\r\n // its reconcile on requestAnimationFrame the same way), then re-verifies once:\r\n // a stray bottom-follow can still pull the landing 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 // 瞬时定位,与官方 TurnNavigator 一致:`use-chat-viewport.ts` 的 `write()`\r\n // 就是直接赋 `scroller.scrollTop`。平滑滚动在长跨度下要跨越整段已加载窗口,\r\n // 窗口失焦时还可能完全不推进——反而比瞬时更不可靠。\r\n scrollport.scrollTop = target\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 await nextFrame()\r\n if (signal?.aborted) return false\r\n scrollToRow()\r\n await delay(600)\r\n if (signal?.aborted) return false\r\n // 单次复核:分页补偿若仍在进行,会把刚落的位置拉回去(见上)。\r\n if (rowDelta() > Math.max(120, scrollport.getBoundingClientRect().height * 0.2)) scrollToRow()\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 // 「提问&回答」**不进导航条**(2026-09-14 用户重新设计):\r\n // 它挂在所属消息的 hover tip 面板里,在 tip 中点击跳转到问答本身。\r\n // 这样导航条只保留一级用户消息,完全规避了「二级条目要缩进 + 一级/二级指示线\r\n // 位置一样无法区分」的层级问题。\r\n const qaMessages = collectQaFromNodes(nodeSnapshot)\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 railSettingsForm !== null ? (cb: () => void) => railSettingsForm!.subscribe(cb) : NOOP_SUB,\r\n () => railSettingsForm?.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 // ── 会话区剩余空间 → 空间不足时隐藏(2026-09-14 用户要求)──────────────────\r\n // 官方右侧边栏在宽屏是「挤压会话区」,窄屏会**完全遮住**会话区。遮住之后这条\r\n // 导航条失去参考物,应该跟着退场(用户原话:「会话区域都被遮挡时,消息导航条\r\n // 也就没有存在意义了,应该也不展示」)。\r\n //\r\n // 定位与显隐都锚在**会话列的右缘**上:官方右侧边栏默认 push 模式(面板展开时\r\n // 会话区让出空间),此时会话列右缘左移,rail 必须跟着走;而 `--dsh-sidebar-width`\r\n // 在当前内核恒为 0px,读它等于把 rail 钉死在视口右缘(右侧边栏一开就被压住)。\r\n //\r\n // 显隐判据(用户拍板):**会话区剩余宽度不足就隐藏**——会话列窄到没有参考价值\r\n // 时退场,右侧边栏造成的挤压同样计入。\r\n const [anchor, setAnchor] = useState<{ right: number, width: number } | null>(null)\r\n useEffect(() => {\r\n const measure = (): void => {\r\n if (typeof document === 'undefined') return\r\n const scrollport = document.querySelector<HTMLElement>('[data-conversation-scroll]')\r\n if (scrollport === null) { setAnchor(null); return }\r\n const rect = scrollport.getBoundingClientRect()\r\n if (rect.width === 0) { setAnchor(null); return }\r\n const next = { right: Math.max(0, Math.round(window.innerWidth - rect.right)), width: Math.round(rect.width) }\r\n // 值不变时沿用旧对象,避免每轮采样都重渲染\r\n setAnchor((prev) => (prev !== null && prev.right === next.right && prev.width === next.width ? prev : next))\r\n }\r\n measure()\r\n const scrollport = document.querySelector<HTMLElement>('[data-conversation-scroll]')\r\n const observer = typeof ResizeObserver === 'undefined' || scrollport === null ? null : new ResizeObserver(measure)\r\n if (observer !== null && scrollport !== null) observer.observe(scrollport)\r\n window.addEventListener('resize', measure)\r\n // 兜底采样:右侧边栏开合改的是它自己的轨道宽度,过渡约 300ms\r\n const timer = window.setInterval(measure, 400)\r\n return () => {\r\n if (observer !== null) observer.disconnect()\r\n window.removeEventListener('resize', measure)\r\n window.clearInterval(timer)\r\n }\r\n }, [sessionId])\r\n const spaceTight = anchor !== null && anchor.width < RAIL_MIN_SPACE\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)\r\n const listRef = 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 // rail 与 tip 之间有间隙:指针从条目移向 tip 时会先触发 item 的 mouseleave。\r\n // 留一点宽限(180ms),并在指针真正进入 tip 时取消,否则 tip 会在指针到达前\r\n // 消失、其中的问答按钮点不到(实测坐标点击无效而元素 .click() 有效即此因)。\r\n const tipCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\r\n const cancelTipClose = () => {\r\n if (tipCloseTimerRef.current !== null) {\r\n clearTimeout(tipCloseTimerRef.current)\r\n tipCloseTimerRef.current = null\r\n }\r\n }\r\n const handleItemLeave = (index: number) => {\r\n cancelTipClose()\r\n tipCloseTimerRef.current = setTimeout(() => {\r\n tipCloseTimerRef.current = null\r\n setTip((prev) => (prev?.index === index ? null : prev))\r\n }, 180)\r\n }\r\n useEffect(() => cancelTipClose, [])\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 // 用户消息锚点是 `13:input-message<id>`;问答条目(②)挂在官方工具调用锚点上,\r\n // 前缀是 `call:`。两者都要扫,否则问答永远算不出「当前位置」。\r\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"],[data-chat-anchor-key^=\"call:\"]')\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 // 滚动的是内层列表容器(rail 自身不滚动,header 不参与滚动)\r\n const el = listRef.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 // 可寻址钩子:挂上真实锚点与问答标记,便于定位/排查(也让实机验证能直接断言)\r\n ...(key !== undefined ? { 'data-crl-anchor': key } : {}),\r\n ...(m.qa === true ? { 'data-crl-qa': 'true' } : {}),\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 // 判断「是否已加载」要用 **node key**:问答条目的 key 是 tool-call 节点键,\r\n // 不是 DOM 锚点(混用会永远判定未加载 → 不滚动)。\r\n nodeKeyOf(m) as string,\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 — the landing itself is\r\n // the visible feedback.\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}` : '') + (spaceTight ? ` ${NAV_HIDDEN_CLASS}` : ''),\r\n // 右缘锚在会话列上:右侧边栏 push 时会话列左移,rail 随之左移(transition 带动画)\r\n ...(anchor !== null ? { style: { right: `${anchor.right + 3}px` } } : {}),\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 // 条目放进独立滚动容器:header(收藏开关)在流内、不再压在条目上\r\n // (sticky header 会盖住滚到它下面的条目——用户截图反馈的「遮挡」)。\r\n createElement('div', { ref: listRef, key: 'list', className: S.list }, ...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 // 指针进入 tip 时取消待关闭定时器(见 handleItemLeave 的宽限说明)\r\n onMouseEnter: () => cancelTipClose(),\r\n onMouseLeave: () => setTip(null),\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 // 只收「本条消息之后、下一条消息之前」的问答——归给前面最近的那条消息。\r\n // 区间用完整 messages(而非收藏过滤后的列表)划分:归属由消息序列决定。\r\n const qaForTip = qaWithinMessage(qaMessages, messages, tipIndex)\r\n if (qaForTip.length > 0) {\r\n children.push(createElement('div', { className: S.tipQaWrap, key: 'qa' },\r\n qaForTip.map((q) => createElement('button', {\r\n type: 'button',\r\n key: q.key ?? q.anchor,\r\n className: S.tipQa,\r\n title: q.text,\r\n onClick: (e: MouseEvent) => {\r\n e.stopPropagation()\r\n if (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 nodeKeyOf(q) as string,\r\n (k) => chatNodeOf(fallbackStore.getSnapshot(), k),\r\n undefined,\r\n controller.signal,\r\n q.seq,\r\n (phase) => setJumping(phase === 'paging'),\r\n ).finally(() => setJumping(false))\r\n },\r\n children: [\r\n // Lucide `circle-help` at a 16-unit grid: outline icon, never a\r\n // rendered glyph (the boxed \"?\" read as an emoji marker).\r\n createElement('svg', {\r\n key: 'icon',\r\n className: S.tipQaIcon,\r\n viewBox: '0 0 16 16',\r\n width: 13,\r\n height: 13,\r\n fill: 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 1.6,\r\n strokeLinecap: 'round',\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n },\r\n createElement('circle', { cx: 8, cy: 8, r: 6.5, 'aria-hidden': true }),\r\n createElement('path', { d: 'M6.06 6a2 2 0 0 1 3.89.67c0 1.33-2 2-2 2', 'aria-hidden': true }),\r\n createElement('path', { d: 'M8 11.33h.01', 'aria-hidden': true })),\r\n createElement('span', { className: S.tipQaText, key: 'txt' }, q.text),\r\n ],\r\n }))))\r\n }\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 // 设置(Plugins 页):0.1.7 的槽是 `plugins.bundle.config`,key 用**包名**。\r\n // 判据(`ui-plugin-manager/src/client/config-ledger.ts`):`plugins.bundle.config` 的\r\n // key 直接取 `entry.options.key`,而 `plugins.row.config` 才用 `包名#行id`\r\n // (`rowConfigKey`);本插件 package.json 声明了 `dsh.bundle.patch`,自身即 bundle。\r\n // 旧的 `settings.plugin.item` 在 0.1.7 client 包里只剩一条注释;替代者 `plugins.item`\r\n // 按契约是官方插件专区(\"OCCUPIED by the official settings pages\"),第三方不该占。\r\n const settingsForm = ctx.configForms.get<RailSettings>('chat-rail')\r\n bindRailSettingsForm(settingsForm)\r\n ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({\r\n name: 'plugins.bundle.config',\r\n key: '@max-null/dsh-chat-rail',\r\n inject: () => ({\r\n setShowOfficialNavigator: (show: boolean) => railSettingsForm?.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 railSettingsForm !== null ? (cb: () => void) => railSettingsForm!.subscribe(cb) : NOOP_SUB,\r\n () => railSettingsForm?.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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8CA,MAAa,SAAS;GAAC;GAAS;GAAY;GAAgB;EAAa;EAIzE,MAAM,UAAoD;GACxD,IAAI;IACF,WAAW;IACX,UAAU;IACV,QAAQ;IACR,UAAU;IACV,UAAU;IACV,SAAS;IACT,aAAa;IACb,aAAa;IACb,WAAW;IACX,UAAU;IACV,KAAK;IACL,OAAO;IACP,MAAM;IACN,QAAQ;IACR,SAAS;IACT,UAAU;IACV,UAAU;IACV,UAAU;IACV,aAAa;IACb,iBAAiB;GACnB;GACA,IAAI;IACF,WAAW;IACX,UAAU;IACV,QAAQ;IACR,UAAU;IACV,UAAU;IACV,SAAS;IACT,aAAa;IACb,aAAa;IACb,WAAW;IACX,UAAU;IACV,KAAK;IACL,OAAO;IACP,MAAM;IACN,QAAQ;IACR,SAAS;IACT,UAAU;IACV,UAAU;IACV,UAAU;IACV,aAAa;IACb,iBAAiB;GACnB;EACF;;EAGA,SAAS,aAAa,IAAY,GAAmC;GACnE,MAAM,OAAO,KAAK,IAAI,IAAI;GAC1B,IAAI,OAAO,KAAQ,OAAO,EAAE;GAC5B,IAAI,OAAO,MAAW,OAAO,EAAE,YAAY,QAAQ,OAAO,OAAO,KAAK,MAAM,OAAO,GAAM,CAAC,CAAC;GAC3F,IAAI,OAAO,OAAY,OAAO,EAAE,UAAU,QAAQ,OAAO,OAAO,KAAK,MAAM,OAAO,IAAS,CAAC,CAAC;GAC7F,IAAI,OAAO,QAAgB,OAAO,EAAE,SAAS,QAAQ,OAAO,OAAO,KAAK,MAAM,OAAO,KAAU,CAAC,CAAC;GACjG,MAAM,IAAI,IAAI,KAAK,EAAE;GACrB,OAAO,GAAG,EAAE,SAAS,IAAI,EAAE,GAAG,EAAE,QAAQ;EAC1C;EASA,MAAM,MAAM;GAYV;GAEA;GACA;GACA;GACA;GAKA;GAeA;GAEA;GAIA;GACA;GACA;GACA;GACA;GASA;GAEA;GACA;GACA;GACA;GACA;GAMA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAEA;GACA;GAKA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAMA;GAEA;GACA;GACA;GACA;GACA;GACA;GAKA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAUA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAEA;GACA;GACA;GACA;GAKA;GAoBA;GACA;GACA;GAKA;GACA;GACA;GACA;GACA;GACA;GAKA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF,CAAC,CAAC,KAAK,EAAE;;;;;EAMT,MAAM,oBAAoB;EAC1B,MAAM,yBAAyB;;;;;;;;EAS/B,MAAM,2BAA2B;;EAGjC,SAAgB,iBAAiB,cAA6B;GAC5D,MAAM,KAAK,SAAS,KAAK,cAAgC,SAAS,uBAAuB,EAAE;GAC3F,IAAI,cAAc;IAChB,IAAI,OAAO,MAAM,GAAG,OAAO;IAC3B;GACF;GACA,IAAI,OAAO,MAAM;IACf,MAAM,MAAM,SAAS,cAAc,OAAO;IAC1C,IAAI,aAAa,wBAAwB,EAAE;IAC3C,IAAI,aAAa,eAAe,wBAAwB;IACxD,IAAI,cAAc;IAClB,SAAS,KAAK,YAAY,GAAG;GAC/B,OAAO,IAAI,GAAG,gBAAgB,mBAE5B,GAAG,cAAc;EAErB;;;;;;EAOA,MAAM,iBAAiB;;EAGvB,MAAM,mBAAmB;;EAGzB,SAAgB,kBAAwB;GACtC,KAAK,MAAM,OAAO,SAAS,iBAA8B,0BAAwB,GAAG;IAClF,KAAK,MAAM,OAAO,IAAI,iBAA8B,iCAAiC,GAAG,IAAI,OAAO;IACnG,IAAI,QAAQ,aAAa;GAC3B;EACF;EAKA,IAAI,mBAIO;;;;;;;;;EAUX,SAAgB,qBAAqB,MAAqB;GACxD,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;GAC/C,mBAAmB;EACrB;;EAGA,MAAM,wBAAwB,CAAC;EAE/B,MAAM,WAAW;EAGjB,IAAI,OAAO,aAAa,aAAa;GACnC,MAAM,WAAW,SAAS,KAAK,cAAgC,0BAA0B,SAAS,GAAG;GACrG,IAAI,aAAa,MAAM;IACrB,MAAM,MAAM,SAAS,cAAc,OAAO;IAC1C,IAAI,QAAQ,SAAS;IACrB,IAAI,QAAQ,YAAY;IACxB,IAAI,cAAc;IAClB,SAAS,KAAK,YAAY,GAAG;GAC/B,OAAO,IAAI,SAAS,gBAAgB,KAClC,SAAS,cAAc;EAE3B;EAEA,MAAM,IAAI;GACR,KAAK;GACL,SAAS;GACT,MAAM;GACN,MAAM;GACN,YAAY;GACZ,OAAO;GACP,KAAK;GACL,MAAM;GACN,KAAK;GACL,MAAM;GACN,KAAK;GACL,SAAS;GACT,cAAc;GACd,KAAK;GACL,WAAW;GACX,OAAO;GACP,WAAW;GACX,WAAW;GACX,SAAS;GACT,YAAY;GACZ,QAAQ;GACR,aAAa;GACb,UAAU;GACV,UAAU;GACV,SAAS;GACT,SAAS;GACT,WAAW;GACX,gBAAgB;EAClB;EAGA,MAAM,aAAa;GAAE,mBAAmB,KAAA;GAAW,uBAAuB,CAAC;EAAE;EAU7E,MAAM,gBAAgB;EAGtB,MAAM,gBAAgB;;EAEtB,IAAI,iBAAsC;EAC1C,MAAM,qCAAqB,IAAI,IAAgB;;EAG/C,SAAS,qBAAmC;GAC1C,IAAI,OAAO,iBAAiB,aAAa,OAAO,CAAC;GACjD,IAAI;IACF,MAAM,MAAM,aAAa,QAAQ,aAAa;IAC9C,IAAI,QAAQ,MAAM,OAAO,CAAC;IAC1B,MAAM,QAAiB,KAAK,MAAM,GAAG;IACrC,OAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAAI,QAAwB,CAAC;GACzG,QAAQ;IACN,OAAO,CAAC;GACV;EACF;EAEA,eAAe,oBAA2C;GACxD,IAAI;IACF,MAAM,MAAM,MAAM,MAAM,eAAe,EAAE,SAAS,EAAE,QAAQ,mBAAmB,EAAE,CAAC;IAClF,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC;IAErB,MAAM,SAAQ,MADK,IAAI,KAAK,EAAA,EACR;IACpB,OAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAAI,QAAwB,CAAC;GACzG,QAAQ;IACN,OAAO,CAAC;GACV;EACF;;EAGM,CAAA,YAAY;GAChB,MAAM,OAAO,MAAM,kBAAkB;GACrC,IAAI,OAAO,WAAW,eAAe,OAAO,iBAAiB,aAAa;IACxE,MAAM,SAAS,mBAAmB;IAClC,IAAI,OAAO,KAAK,IAAI,CAAC,CAAC,WAAW,KAAK,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GACjE,MAAM,MAAM,eAAe;KACzB,QAAQ;KAAO,SAAS,EAAE,gBAAgB,mBAAmB;KAC7D,MAAM,KAAK,UAAU,EAAE,WAAW,OAAO,CAAC;IAC5C,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;IAEnB,IAAI;KAAE,aAAa,WAAW,aAAa;IAAE,QAAQ,CAAW;IAEhE,iBAAiB,OAAO,KAAK,IAAI,CAAC,CAAC,WAAW,IAAI,SAAS;IAE3D,KAAK,MAAM,YAAY,oBAAoB,SAAS;GACtD,OACE,iBAAiB;EAErB,EAAA,CAAG;;EAGH,SAAgB,gBAA8B;GAC5C,IAAI,mBAAmB,MAAM,OAAO;GACpC,OAAO,mBAAmB;EAC5B;;EAGA,SAAS,eAAe,KAAyB;GAC/C,iBAAiB;GACjB,MAAW,eAAe;IACxB,QAAQ;IAAO,SAAS,EAAE,gBAAgB,mBAAmB;IAC7D,MAAM,KAAK,UAAU,EAAE,WAAW,IAAI,CAAC;GACzC,CAAC,CAAC,CAAC,YAAY,CAA2B,CAAC;EAC7C;;EAGA,SAAgB,cAAc,KAAmB,WAA6B;GAC5E,MAAM,OAAO,IAAI;GACjB,OAAO,MAAM,QAAQ,IAAI,IAAI,KAAK,QAAQ,OAAqB,OAAO,OAAO,QAAQ,IAAI,CAAC;EAC5F;;EAGA,SAAgB,iBAAiB,MAAgB,IAAsB;GACrE,OAAO,KAAK,SAAS,EAAE,IAAI,KAAK,QAAQ,cAAc,cAAc,EAAE,IAAI,CAAC,GAAG,MAAM,EAAE;EACxF;;EAGA,SAAgB,WAAW,KAAmB,WAAmB,WAA4B;GAC3F,OAAO,cAAc,KAAK,SAAS,CAAC,CAAC,SAAS,SAAS;EACzD;;EAGA,SAAS,oBAAkC;GACzC,IAAI,mBAAmB,MAAM,iBAAiB,cAAc;GAC5D,OAAO;EACT;;EAGA,SAAS,mBAAmB,UAAkC;GAC5D,mBAAmB,IAAI,QAAQ;GAC/B,aAAa;IAAE,mBAAmB,OAAO,QAAQ;GAAE;EACrD;;EAGA,SAAS,eAAe,WAAmB,WAAyB;GAClE,MAAM,OAAO,EAAE,GAAG,kBAAkB,EAAE;GACtC,KAAK,aAAa,iBAAiB,cAAc,kBAAkB,GAAG,SAAS,GAAG,SAAS;GAC3F,iBAAiB;GACjB,eAAe,IAAI;GACnB,KAAK,MAAM,YAAY,CAAC,GAAG,kBAAkB,GAAG,SAAS;EAC3D;;EAGA,SAAgB,qBAAqB,KAAqB;GACxD,OAAO,IAAI,WAAW,kBAAkB,IAAI,IAAI,MAAM,EAAyB,IAAI;EACrF;;;;EAKA,SAAS,oBAAoB,GAA4C;GACvE,IAAI,OAAO,EAAE,OAAO,YAAY,EAAE,OAAO,IAAI,OAAO,EAAE;GACtD,IAAI,OAAO,EAAE,QAAQ,UAAU,OAAO,qBAAqB,EAAE,GAAG;GAChE,OAAO;EACT;EAaA,MAAM,YAIF;GACF,WAAW,KAAA;GACX,wBAAwB,CAAC;GACzB,cAAc,CAAC;EACjB;;;EAIA,MAAM,mCAAmB,IAAI,IAAI;GAAC;GAAM;GAAQ;GAAO;EAAQ,CAAC;;EAGhE,SAAS,aAAa,KAA4C;GAChE,KAAK,MAAM,UAAU,IAAI,iBAAoC,QAAQ,GAAG;IACtE,MAAM,QAAQ,OAAO,aAAa,YAAY;IAC9C,IAAI,UAAU,QAAQ,iBAAiB,IAAI,KAAK,GAAG,OAAO;GAC5D;GACA,OAAO;EACT;;EAGA,MAAM,YAAY;;EAGlB,SAAS,iBAAiB,WAAmB,MAAiD;GAC5F,MAAM,SAAS,SAAS,cAAc,QAAQ;GAC9C,OAAO,OAAO;GACd,OAAO,YAAY;GACnB,OAAO,QAAQ,UAAU;GACzB,MAAM,eAAe,WAAW,kBAAkB,GAAG,UAAU,aAAa,IAAI,SAAS,IAAI,KAAK,QAAQ,KAAK;GAC/G,OAAO,aAAa,gBAAgB,OAAO,WAAW,kBAAkB,GAAG,UAAU,aAAa,IAAI,SAAS,CAAC,CAAC;GACjH,OAAO,QAAQ,UAAU,WAAW,kBAAkB,GAAG,UAAU,aAAa,IAAI,SAAS,IAAI,SAAS,KAAA;GAC1G,OAAO,UAAU,OAAO,UAAU,WAAW,kBAAkB,GAAG,UAAU,aAAa,IAAI,SAAS,CAAC;GAIvG,OAAO,aAAa,YAAY,YAAY;GAC5C,OAAO,aAAa,cAAc,YAAY;GAC9C,OAAO,YAAY,0JAA0J,UAAU;GACvL,OAAO,iBAAiB,UAAU,UAAU;IAC1C,MAAM,eAAe;IACrB,MAAM,gBAAgB;IAGtB,MAAM,KAAK,OAAO,QAAQ;IAC1B,IAAI,OAAO,KAAA,GAAW;KACpB,UAAU,iBAAiB,EAAE;KAC7B,sBAAsB,QAAQ,IAAI;IACpC;GACF,CAAC;GACD,OAAO;EACT;;EAGA,SAAS,aAAa,WAAmB,MAAiD;GACxF,MAAM,SAAS,SAAS,cAAc,QAAQ;GAC9C,OAAO,OAAO;GACd,OAAO,YAAY;GACnB,OAAO,QAAQ,UAAU;GACzB,OAAO,aAAa,YAAY,KAAK,IAAI;GACzC,OAAO,aAAa,cAAc,KAAK,IAAI;GAC3C,OAAO,YAAY;GACnB,OAAO,iBAAiB,UAAU,UAAU;IAC1C,MAAM,eAAe;IACrB,MAAM,gBAAgB;IACtB,MAAM,KAAK,OAAO,QAAQ;IAC1B,IAAI,OAAO,KAAA,GAAW,UAAU,OAAO,EAAE;GAC3C,CAAC;GACD,OAAO;EACT;;EAGA,SAAS,sBAAsB,QAA2B,MAAoC;GAC5F,MAAM,KAAK,OAAO,QAAQ;GAC1B,IAAI,OAAO,KAAA,GAAW;GACtB,MAAM,UAAU,WAAW,kBAAkB,GAAG,UAAU,aAAa,IAAI,EAAE;GAC7E,OAAO,aAAa,gBAAgB,OAAO,OAAO,CAAC;GACnD,OAAO,QAAQ,UAAU,UAAU,SAAS,KAAA;GAC5C,OAAO,UAAU,OAAO,UAAU,OAAO;GACzC,MAAM,QAAQ,UAAU,KAAK,QAAQ,KAAK;GAE1C,OAAO,aAAa,YAAY,KAAK;GACrC,OAAO,aAAa,cAAc,KAAK;EACzC;;EAGA,SAAS,0BAA0B,MAAoC;GACrE,KAAK,MAAM,UAAU,SAAS,iBAAoC,iBAAiB,GACjF,sBAAsB,QAAQ,IAAI;EAEtC;;EAGA,SAAS,qBAAqB,KAAkB,MAAoC;GAElF,IAAI,IAAI,QAAQ,eAAe,KACzB;QAAA,IAAI,cAAc,iBAAiB,MAAM,QAAQ,IAAI,cAAc,iBAAiB,MAAM,MAAM;GAAA;GAGtG,MAAM,OAAO,aAAa,GAAG;GAC7B,MAAM,YAAY,qBAAqB,IAAI,aAAa,sBAAsB,KAAK,EAAE;GACrF,IAAI,SAAS,QAAQ,cAAc,IAAI;GACvC,MAAM,OAAO,iBAAiB,WAAW,IAAI;GAC7C,KAAK,QAAQ,UAAU,WAAW,kBAAkB,GAAG,UAAU,aAAa,IAAI,SAAS,IAAI,SAAS,KAAA;GACxG,KAAK,UAAU,OAAO,UAAU,WAAW,kBAAkB,GAAG,UAAU,aAAa,IAAI,SAAS,CAAC;GACrG,MAAM,OAAO,aAAa,WAAW,IAAI;GACzC,KAAK,OAAO,MAAM,IAAI;GACtB,IAAI,QAAQ,aAAa;EAC3B;;EAGA,SAAS,oBAAoB,MAA0C;GACrE,MAAM,aAAa;IACjB,KAAK,MAAM,OAAO,SAAS,iBAA8B,8CAA4C,GACnG,qBAAqB,KAAK,IAAI;GAElC;GACA,MAAM,WAAW,IAAI,iBAAiB,IAAI;GAC1C,IAAI,OAAO,aAAa,eAAe,SAAS,SAAS,MACvD,SAAS,QAAQ,SAAS,MAAM;IAAE,WAAW;IAAM,SAAS;GAAK,CAAC;GAEpE,KAAK;GACL,aAAa,SAAS,WAAW;EACnC;;EA2CA,SAAS,aAAa,SAA+B;GACnD,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO,CAAC;GACrC,MAAM,MAAmB,CAAC;GAC1B,KAAK,MAAM,SAAS,SAAS;IAC3B,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;IACjD,MAAM,IAAI;IACV,IAAI,EAAE,SAAS,SAAS;IACxB,MAAM,IAAI,EAAE;IACZ,IAAI,MAAM,QAAQ,OAAO,MAAM,UAAU;IACzC,MAAM,MAAM;IACZ,IAAI,OAAO,IAAI,iBAAiB,YAAY,IAAI,iBAAiB,IAAI;IACrE,IAAI,KAAK;KACP,cAAc,IAAI;KAClB,WAAW,OAAO,IAAI,cAAc,WAAW,IAAI,YAAY;KAC/D,OAAO,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ;KACnD,QAAQ,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS;IACxD,CAAC;GACH;GACA,OAAO;EACT;;;EAIA,SAAS,oBAAoB,SAA+B;GAC1D,MAAM,QAAqB,CAAC;GAC5B,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;GACpC,KAAK,MAAM,SAAS,SAAS;IAC3B,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;IACjD,MAAM,IAAI;IACV,IAAI,EAAE,SAAS,SAAS;IACxB,MAAM,IAAI,EAAE;IACZ,IAAI,MAAM,QAAQ,OAAO,MAAM,UAAU;KACvC,MAAM,MAAM;KACZ,IAAI,OAAO,IAAI,iBAAiB,YAAY,IAAI,iBAAiB,IAAI;MACnE,MAAM,KAAK;OAAE,MAAM;OAAO,cAAc,IAAI;OAAc,WAAW,OAAO,IAAI,cAAc,WAAW,IAAI,YAAY;MAAY,CAAC;MACtI;KACF;IACF;IACA,IAAI,OAAO,EAAE,SAAS,YAAY,EAAE,SAAS,IAC3C,MAAM,KAAK;KAAE,MAAM;KAAQ,KAAK,QAAQ,OAAO,EAAE,cAAc,WAAW,EAAE,YAAY,YAAY,UAAU,EAAE;IAAO,CAAC;GAE5H;GACA,OAAO;EACT;;EAGA,SAAS,UAAU,GAAgC;GACjD,IAAI,MAAM,QAAQ,OAAO,MAAM,UAAU,OAAO;GAChD,MAAM,IAAI;GACV,IAAI,OAAO,EAAE,QAAQ,UAAU,OAAO;GAGtC,MAAM,OAAO,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU;GAC/F,MAAM,SAAS,MAAM,QAAQ,EAAE,MAAM,IACjC,EAAE,OAAO,IAAI,iBAAiB,CAAC,CAAC,QAAQ,MAAsB,MAAM,IAAI,IACxE,KAAA;GACJ,OAAO;IACL,KAAK,EAAE;IACP,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;IAC5C;IAEA,UAAU,EAAE,aAAa;IACzB,GAAI,WAAW,KAAA,KAAa,OAAO,SAAS,IAAI,EAAE,OAAO,IAAI,CAAC;IAC9D,GAAI,OAAO,EAAE,QAAQ,WAAW,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC;IAClD,GAAI,OAAO,EAAE,OAAO,WAAW,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;GACjD;EACF;;EAGA,SAAS,kBAAkB,GAA8B;GACvD,IAAI,MAAM,QAAQ,OAAO,MAAM,UAAU,OAAO;GAChD,MAAM,IAAI;GACV,IAAI,OAAO,EAAE,iBAAiB,YAAY,EAAE,iBAAiB,IAAI,OAAO;GACxE,OAAO;IACL,cAAc,EAAE;IAChB,WAAW,OAAO,EAAE,cAAc,WAAW,EAAE,YAAY;IAC3D,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ;IAC/C,QAAQ,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;GACpD;EACF;;EAGA,SAAS,WAAW,SAA0B;GAC5C,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;GACpC,IAAI,MAAM;GACV,KAAK,MAAM,SAAS,SAClB,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAa,MAA6B,SAAS,UACrF,OAAQ,MAA6B,SAAS,UACjD,OAAQ,MAA2B;GAGvC,OAAO,IAAI,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE;EAC/B;;EAGA,SAAS,aAAa,SAA2B;GAC/C,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;GACpC,OAAO,QAAQ,MAAK,UAAS,UAAU,QAAQ,OAAO,UAAU,YAC1D,MAA6B,SAAS,OAAO;EACrD;;;;;;;;;EAmDA,SAAgB,WAAW,UAAmB,KAAkC;GAC9E,IAAI,QAAQ,KAAA,KAAa,aAAa,KAAA,KAAa,aAAa,MAAM,OAAO,KAAA;GAC7E,MAAM,QAAS,UAA2C;GAC1D,IAAI,UAAU,KAAA,KAAa,OAAO,MAAM,QAAQ,YAC9C,OAAO,MAAM,IAAI,GAAG;GAGtB,QADa,UAAsE,MAAM,MAAA,EAC7E,IAAI,GAAG;EACrB;;EAGA,SAAgB,kBAAkB,MAAmC;GACnE,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO;GACtD,MAAM,IAAI;GACV,IAAI,EAAE,SAAS,UAAU,EAAE,SAAS,YAAY,OAAO;GACvD,MAAM,MAAM,OAAO,EAAE,QAAQ,WAAW,EAAE,MAAM,KAAA;GAChD,IAAI,QAAQ,KAAA,GAAW,OAAO;GAC9B,MAAM,OAAO,EAAE;GACf,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO;GACtD,IAAI,OAAO,KAAK,SAAS,YAAY,CAAC,MAAM,QAAQ,KAAK,OAAO,GAAG,OAAO;GAC1E,MAAM,SAAS,aAAa,KAAK,OAAO;GACxC,OAAO;IACL,KAAK,OAAO,EAAE,cAAc,WAAW,EAAE,YAAY;IACrD,MAAM,KAAK;IACX,MAAM,WAAW,KAAK,OAAO;IAC7B,UAAU,aAAa,KAAK,OAAO;IACnC,GAAI,OAAO,SAAS,IAAI,EAAE,OAAO,IAAI,CAAC;IACtC;GACF;EACF;;;;EAKA,SAAgB,iBAAiB,UAAkC;GACjE,MAAM,MAAqB,CAAC;GAC5B,MAAM,QAAS,UAA2C;GAC1D,IAAI,UAAU,KAAA,KAAa,OAAO,MAAM,WAAW,YACjD,KAAK,MAAM,QAAQ,MAAM,OAAO,GAAG;IACjC,MAAM,IAAI,kBAAkB,IAAI;IAChC,IAAI,MAAM,MAAM,IAAI,KAAK,CAAC;GAC5B;QACK;IACL,MAAM,OAAQ,UAAuE;IACrF,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO;IACtC,KAAK,MAAM,QAAQ,KAAK,MAAM,OAAO,GAAG;KACtC,MAAM,IAAI,kBAAkB,IAAI;KAChC,IAAI,MAAM,MAAM,IAAI,KAAK,CAAC;IAC5B;GACF;GACA,IAAI,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG;GAChC,OAAO;EACT;;EAGA,SAAS,aAAa,UAA8B;GAClD,MAAM,QAAS,UAA2C;GAC1D,IAAI,UAAU,KAAA,KAAa,OAAO,MAAM,WAAW,YACjD,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC;GAE3B,MAAM,OAAQ,UAAuE;GACrF,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,CAAC;GACvC,OAAO,CAAC,GAAG,KAAK,MAAM,OAAO,CAAC;EAChC;;;;;;;;;;;;;;;;;;;EAoBA,SAAgB,mBAAmB,UAAkC;GACnE,MAAM,MAAqB,CAAC;GAC5B,KAAK,MAAM,QAAQ,aAAa,QAAQ,GAAG;IACzC,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU;IAC/C,MAAM,IAAI;IAIV,IAAI,EAAE,SAAS,aAAa;IAC5B,MAAM,OAAQ,EAAE,MAAyC;IACzD,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU;IAC/C,MAAM,IAAI;IAQV,MAAM,SAAS,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;IACzD,IAAI,WAAW,IAAI;IAGnB,KADa,OAAO,EAAE,SAAS,WAAW,EAAE,OAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,EAAE,KAAK,OAAO,QACxF,qBAAqB;IAElC,MAAM,QAAQ,gBADE,OAAO,EAAE,YAAY,WAAW,EAAE,UAAW,OAAO,EAAE,MAAM,YAAY,WAAW,EAAE,KAAK,UAAU,EAC/E;IACrC,IAAI,UAAU,IAAI;IAClB,IAAI,KAAK;KACP,KAAK,OAAO,EAAE,cAAc,WAAW,EAAE,YAAY;KACrD,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;KAC5C,MAAM;KACN,UAAU;KAEV,KAAK,OAAQ,EAAwB,QAAQ,WAAY,EAAsB,MAAM,KAAA;KAErF,QAAQ,QAAQ;KAChB,IAAI;IACN,CAAC;GACH;GACA,IAAI,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG;GAChC,OAAO;EACT;;;;;;;;;;;;;;EAeA,SAAgB,gBACd,YACA,UACA,OACe;GACf,MAAM,OAAO,SAAS;GACtB,IAAI,SAAS,KAAA,GAAW,OAAO,CAAC;GAChC,MAAM,OAAO,SAAS,QAAQ;GAC9B,MAAM,QAAQ,SAAS,KAAA,IAAY,KAAA,IAAY,KAAK;GACpD,OAAO,WAAW,QAAQ,MAAM,EAAE,OAAO,KAAK,QAAQ,UAAU,KAAA,KAAa,EAAE,MAAM,MAAM;EAC7F;;EAGA,SAAS,gBAAgB,SAA0B;GACjD,IAAI,OAAO,YAAY,YAAY,QAAQ,KAAK,MAAM,IAAI,OAAO;GACjE,IAAI;GACJ,IAAI;IAAE,SAAS,KAAK,MAAM,OAAO;GAAE,QAAQ;IAAE,OAAO;GAAG;GACvD,MAAM,OAAQ,QAA2C;GACzD,IAAI,CAAC,MAAM,QAAQ,IAAI,GAAG,OAAO;GACjC,MAAM,QAAkB,CAAC;GACzB,KAAK,MAAM,KAAK,MAAM;IACpB,IAAI,MAAM,QAAQ,OAAO,MAAM,UAAU;IACzC,MAAM,IAAI;IACV,MAAM,OAAO,OAAO,EAAE,aAAa,YAAY,EAAE,aAAa,KAC1D,EAAE,WACF,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;IAC9C,IAAI,SAAS,IAAI,MAAM,KAAK,IAAI;GAClC;GACA,OAAO,MAAM,KAAK,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE;EAC7C;;EAGA,SAAS,YAAY,GAAoC;GAEvD,IAAI,OAAO,EAAE,WAAW,YAAY,EAAE,WAAW,IAAI,OAAO,EAAE;GAC9D,IAAI,OAAO,EAAE,QAAQ,YAAY,EAAE,QAAQ,IAAI,OAAO,EAAE;GACxD,IAAI,OAAO,EAAE,OAAO,YAAY,EAAE,OAAO,IAAI,OAAO,qBAAqB,EAAE;EAE7E;;EAGA,SAAS,UAAU,GAAoC;GACrD,OAAO,OAAO,EAAE,QAAQ,YAAY,EAAE,QAAQ,KAAK,EAAE,MAAM,YAAY,CAAC;EAC1E;;;EAIA,SAAS,WAAW,GAAgB,cAA+B;GAGjE,MAAM,UADO,WAAW,cADZ,YAAY,CACgB,CACrB,CAAC,EAAE,MAAM;GAC5B,IAAI,MAAM,QAAQ,OAAO,GAAG;IAC1B,IAAI,MAAM;IACV,KAAK,MAAM,SAAS,SAClB,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAa,MAA6B,SAAS,UACrF,OAAQ,MAA6B,SAAS,UACjD,OAAQ,MAA2B;IAGvC,MAAM,OAAO,IAAI,KAAK;IACtB,IAAI,SAAS,IAAI,OAAO;GAC1B;GACA,OAAO,EAAE;EACX;;;;EAKA,SAAS,YAAY,GAAgB,cAAoC;GACvE,IAAI,MAAM,QAAQ,EAAE,MAAM,KAAK,EAAE,OAAO,SAAS,GAC/C,OAAO,EAAE,OAAO,KAAI,SAAQ;IAAE,MAAM;IAAO,cAAc,IAAI;IAAc,WAAW,IAAI;GAAU,EAAE;GAIxG,OAAO,oBADM,WAAW,cADZ,YAAY,CACgB,CACV,CAAC,EAAE,MAAM,OAAO;EAChD;EAEA,MAAM,SAAS,OAAe,IAAI,SAAe,YAAY,WAAW,SAAS,EAAE,CAAC;;EAGpF,MAAM,kBAAiC,IAAI,SAAe,YAAY;GACpE,IAAI,OAAO,0BAA0B,YAAY;IAAE,QAAQ;IAAG;GAAO;GACrE,4BAA4B;IAAE,QAAQ;GAAE,CAAC;EAC3C,CAAC;;;;;;;;;;;;;;;;;;;;;;;EAwBD,eAAe,cACb,iBACA,WACA,KAEA,QACA,YACA,QAEA,WAKA,QACkB;GAClB,MAAM,UAAU,gBAAgB,QAAQ,SAAS,CAAC,EAAE;GACpD,IAAI,YAAY,KAAA,GAAW,OAAO;GAClC,MAAM,WAAW,WAAW,KAAA,KACvB,MAAe,QAAQ,YAAY,CAAC,CAAiD,MAAM,OAAO,IAAI,CAAC,MAAM,KAAA,IAC9G;GACJ,IAAI,QAAQ;GACZ,IAAI,SAAS;GACb,MAAM,cAAc;GACpB,IAAI,OAAO,YAAY,gBAAgB,cAAc,cAAc,KAAA,KAAa,OAAO,cAAc,SAAS,GAAG;IAI/G,IAAI,CAAC,SAAS,GAAG,GAAG;KAClB,IAAI,OAAO;KACX,OAAO,SAAS,KAAK;MACnB,IAAI,QAAQ,SAAS,OAAO;MAE5B,IADiB,QAAQ,YACd,CAAC,EAAE,iBAAiB,MAAM;MACrC,MAAM,MAAM,EAAE;KAChB;KACA,IAAI,QAAQ,SAAS,OAAO;KAC5B,SAAS,QAAQ;KACjB,MAAM,YAAY,YAAY,SAAS;KAIvC,IAAI,WAAW;KACf,OAAO,aAAa,KAAK;MACvB,IAAI,QAAQ,SAAS,OAAO;MAC5B,IAAI,SAAS,GAAG,GAAG;OAAE,SAAS;OAAM;MAAM;MAC1C,MAAM,MAAM,EAAE;KAChB;KACA,IAAI,CAAC,QAAQ;MACX,QAAQ,KAAK,oCAAoC,IAAI,iCAAiC,OAAO,SAAS,EAAE,EAAE;MAC1G,OAAO;KACT;IACF;IAIA,SAAS;GACX,OAAO;IACL,IAAI,QAAQ;IACZ,IAAI,iBAAiB;IACrB,OAAO,UAAU,KAAK;KACpB,IAAI,QAAQ,SAAS,OAAO;KAC5B,MAAM,WAAW,QAAQ,YAAY;KACrC,IAAI,SAAS,GAAG,GAAG;MACjB,SAAS;MACT;KACF;KACA,IAAI,UAAU,YAAY,MAAM;KAChC,IAAI,SAAS,iBAAiB,MAAM;MAElC,MAAM,MAAM,EAAE;MACd;KACF;KACA,IAAI,CAAC,gBAAgB;MAAE,SAAS,QAAQ;MAAG,iBAAiB;KAAK;KACjE,MAAM,QAAQ,UAAU;KACxB;KAEA,aAAa,KAAK;IACpB;IAGA,IAAI,CAAC,QAAQ;KACX,QAAQ,KAAK,oCAAoC,IAAI,qBAAqB,MAAM,SAAS;KACzF,OAAO;IACT;GACF;GACA,MAAM,aAAa,OAAO,aAAa,cAAc,SAAS,cAAc,4BAA4B,IAAI;GAC5G,IAAI,eAAe,MAAM,OAAO;GAIhC,IAAI,MAAsB;GAC1B,IAAI,SAAS;GACb,OAAO,WAAW,KAAK;IACrB,IAAI,QAAQ,SAAS,OAAO;IAC5B,MAAM,WAAW,cAAc,0BAA0B,IAAI,OAAO,GAAG,EAAE,GAAG;IAC5E,IAAI,QAAQ,MAAM;IAClB,MAAM,MAAM,EAAE;GAChB;GACA,IAAI,QAAQ,MAAM,OAAO;GAGzB,SAAS,QAAQ;GAQjB,MAAM,oBAA0B;IAC9B,MAAM,WAAW,WAAW,sBAAsB;IAClD,MAAM,UAAW,IAAgB,sBAAsB;IAKvD,MAAM,SAAS,KAAK,IAClB,GACA,WAAW,aAAa,QAAQ,MAAM,SAAS,QAAQ,SAAS,SAAS,QAAQ,UAAU,CAC7F;IAIA,WAAW,YAAY;GACzB;GACA,MAAM,iBAAyB;IAC7B,MAAM,WAAW,WAAW,sBAAsB;IAClD,MAAM,UAAW,IAAgB,sBAAsB;IACvD,OAAO,KAAK,IAAK,QAAQ,MAAM,QAAQ,SAAS,KAAM,SAAS,MAAM,SAAS,SAAS,EAAE;GAC3F;GACA,MAAM,UAAU;GAChB,IAAI,QAAQ,SAAS,OAAO;GAC5B,YAAY;GACZ,MAAM,MAAM,GAAG;GACf,IAAI,QAAQ,SAAS,OAAO;GAE5B,IAAI,SAAS,IAAI,KAAK,IAAI,KAAK,WAAW,sBAAsB,CAAC,CAAC,SAAS,EAAG,GAAG,YAAY;GAC7F,OAAO;EACT;;;;EAOA,MAAM,4BAAY,IAAI,IAA6B;;EAYnD,SAAS,eAAe,MAAkB,WAA2B;GACnE,IAAI,SAAS;GACb,MAAM,QAAQ;GACd,KAAK,IAAI,SAAS,GAAG,SAAS,KAAK,QAAQ,UAAU,OACnD,UAAU,OAAO,aAAa,GAAG,KAAK,SAAS,QAAQ,SAAS,KAAK,CAAC;GAExE,OAAO,QAAQ,UAAU,UAAU,KAAK,MAAM;EAChD;;EAGA,SAAS,aAAa,WAAmB,MAAwB,OAAuE;GACtI,MAAM,MAAM,GAAG,UAAU,GAAG,MAAM;GAClC,IAAI,UAAU,UAAU,IAAI,GAAG;GAC/B,IAAI,YAAY,KAAA,GAAW;IACzB,UAAU,KAAK,MAAM,YAAY,CAAC,CAAC,MAAM,WAAmB;KAC1D,IAAI,CAAC,OAAO,MAAM,OAAO,UAAU,KAAA,GACjC,MAAM,IAAI,MAAM,OAAO,OAAO,WAAW,OAAO,OAAO,QAAQ,uBAAuB;KAExF,MAAM,OAAO,OAAO,MAAM;KAC1B,IAAI,SAAS,KAAA,GAAW,MAAM,IAAI,MAAM,kCAAkC;KAC1E,MAAM,YAAY,OAAO,MAAM,YAAY,aAAa,MAAM;KAC9D,IAAI,OAAO,IAAI,oBAAoB,YACjC,OAAO,IAAI,gBAAgB,IAAI,KAAK,CAAC,IAAgB,GAAG,EAAE,MAAM,UAAU,CAAC,CAAC;KAE9E,OAAO,eAAe,MAAM,SAAS;IACvC,CAAC;IACD,QAAa,YAAY;KAAE,UAAU,OAAO,GAAG;IAAE,CAAC;IAClD,UAAU,IAAI,KAAK,OAAO;GAC5B;GACA,OAAO;EACT;;;EAIA,SAAS,SAAS,EAAE,MAAM,WAAW,QAIvB;GACZ,MAAM,CAAC,KAAK,WAAA,GAAUA,MAAAA,SAAAA,CAAwB,IAAI;GAClD,MAAM,CAAC,QAAQ,cAAA,GAAaA,MAAAA,SAAAA,CAAS,KAAK;GAC1C,MAAM,UAAU,KAAK,SAAS,QAAQ,OAAO,KAAK,iBAAiB;GACnE,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,QAAQ;IACZ,OAAO,IAAI;IACX,UAAU,KAAK;IACf,IAAI,KAAK,SAAS,QAAQ;KACxB,OAAO,KAAK,GAAG;KACf,aAAa;MAAE,QAAQ;KAAM;IAC/B;IACA,aAAa,WAAW,MAAM,IAAI,CAAC,CAChC,MAAM,QAAQ;KAAE,IAAI,OAAO,OAAO,GAAG;IAAE,CAAC,CAAC,CACzC,YAAY;KAAE,IAAI,OAAO,UAAU,IAAI;IAAE,CAAC;IAC7C,aAAa;KAAE,QAAQ;IAAM;GAG/B,GAAG;IAAC;IAAS;IAAW;GAAI,CAAC;GAC7B,IAAI,QAAQ,OAAO;GACnB,IAAI,QAAQ,MAAM,QAAA,GAAOC,MAAAA,cAAAA,CAAc,OAAO;IAAE,WAAW,EAAE;IAAU,eAAe;GAAK,CAAC;GAC5F,QAAA,GAAOA,MAAAA,cAAAA,CAAc,OAAO;IAAE,WAAW,EAAE;IAAQ;IAAK,KAAK;IAAI,WAAW;GAAM,CAAC;EACrF;;EAqBA,SAAS,cAAsC;GAC7C,MAAM,OAAO,OAAO,aAAa,eAAe,SAAS,gBAAgB,QAAQ,KAAA,CAAM,YAAY,IAAI;GACvG,OAAO,QAAQ,KAAK,WAAW,IAAI,IAAI,OAAO;EAChD;EAEA,SAAS,aAAa,EAAE,eAAe,WAAW,iBAAiB,QAAQ,cAAc,gBAA8C;GACrI,MAAM,IAAI,YAAY;GACtB,MAAM,YAAY,cAAc,UAAU;GAC1C,MAAM,UAAU,cAAc,KAAA,IAAY,KAAA,IAAa,gBAAgB,QAAQ,SAAS,CAAC,EAAkG;GAK3L,MAAM,iBADa,cAAc,KAAA,IAAY,KAAA,IAAY,SAAS,SAAS,OACtC,YAAY,KAAA,IAAY,aAAa;GAC1E,MAAM,gBAAA,GAAeC,MAAAA,qBAAAA,EAClB,OAAO,cAAc,UAAU,EAAE,SAC5B,cAAc,YAAY,CAClC;GAEA,IAAI,WAA0B,CAAC;GAC/B,IAAI,MAAM,QAAQ,WAAW,QAAQ,KAAK,UAAU,SAAS,SAAS,GACpE,WAAW,UAAU,SAAS,IAAI,SAAS,CAAC,CAAC,QAAQ,MAAwB,MAAM,IAAI;GAEzF,IAAI,SAAS,WAAW,GACtB,WAAW,iBAAiB,YAAY;GAM1C,MAAM,aAAa,mBAAmB,YAAY;GASlD,MAAM,gBAAA,GAJmBA,MAAAA,qBAAAA,CACvB,qBAAqB,QAAQ,OAAmB,iBAAkB,UAAU,EAAE,IAAI,gBAC5E,kBAAkB,YAAY,KAAK,EAAE,OAAO,KAAA,EAAU,CAE1B,CAAC,CAAC,OAAO,0BAA0B;GACvE,MAAM,qBAAqB,OAAsB;IAC/C,iBAAiB,EAAE;IACnB,IAAI,IAAI;KACN,gBAAgB;KAChB,IAAI,OAAO,YAAY,MAAM,OAAO,QAAQ,MAAM,UAAU;IAC9D,OAAO,IAAI,OAAO,YAAY,MAC5B,OAAO,QAAQ,MAAM,UAAU;GAEnC;GACA,CAAA,GAAA,MAAA,UAAA,OAAgB;IAAE,kBAAkB,YAAY;GAAE,GAAG,CAAC,YAAY,CAAC;GAanE,MAAM,CAAC,QAAQ,cAAA,GAAaF,MAAAA,SAAAA,CAAkD,IAAI;GAClF,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,MAAM,gBAAsB;KAC1B,IAAI,OAAO,aAAa,aAAa;KACrC,MAAM,aAAa,SAAS,cAA2B,4BAA4B;KACnF,IAAI,eAAe,MAAM;MAAE,UAAU,IAAI;MAAG;KAAO;KACnD,MAAM,OAAO,WAAW,sBAAsB;KAC9C,IAAI,KAAK,UAAU,GAAG;MAAE,UAAU,IAAI;MAAG;KAAO;KAChD,MAAM,OAAO;MAAE,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,aAAa,KAAK,KAAK,CAAC;MAAG,OAAO,KAAK,MAAM,KAAK,KAAK;KAAE;KAE7G,WAAW,SAAU,SAAS,QAAQ,KAAK,UAAU,KAAK,SAAS,KAAK,UAAU,KAAK,QAAQ,OAAO,IAAK;IAC7G;IACA,QAAQ;IACR,MAAM,aAAa,SAAS,cAA2B,4BAA4B;IACnF,MAAM,WAAW,OAAO,mBAAmB,eAAe,eAAe,OAAO,OAAO,IAAI,eAAe,OAAO;IACjH,IAAI,aAAa,QAAQ,eAAe,MAAM,SAAS,QAAQ,UAAU;IACzE,OAAO,iBAAiB,UAAU,OAAO;IAEzC,MAAM,QAAQ,OAAO,YAAY,SAAS,GAAG;IAC7C,aAAa;KACX,IAAI,aAAa,MAAM,SAAS,WAAW;KAC3C,OAAO,oBAAoB,UAAU,OAAO;KAC5C,OAAO,cAAc,KAAK;IAC5B;GACF,GAAG,CAAC,SAAS,CAAC;GACd,MAAM,aAAa,WAAW,QAAQ,OAAO,QAAQ;GAKrD,MAAM,aAAA,GAAYE,MAAAA,qBAAAA,CAAqB,oBAAoB,iBAAiB;GAC5E,MAAM,CAAC,SAAS,eAAA,GAAcF,MAAAA,SAAAA,CAAS,KAAK;GAO5C,MAAM,iBAHW,cAAc,KAAA,IAC3B,IACA,SAAS,QAAQ,MAAM,WAAW,WAAW,WAAW,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,UACpD;GACjC,MAAM,oBAAoB,UACtB,SAAS,QAAQ,MAAM,cAAc,KAAA,KAAa,WAAW,WAAW,WAAW,oBAAoB,CAAC,CAAC,CAAC,IAC1G;GAMJ,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,WAAW,CAAC,eAAe,WAAW,KAAK;GACjD,GAAG,CAAC,SAAS,aAAa,CAAC;GAE3B,MAAM,CAAC,aAAa,mBAAA,GAAkBA,MAAAA,SAAAA,CAAS,EAAE;GACjD,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,KAAK;GACtC,MAAM,CAAC,SAAS,eAAA,GAAcA,MAAAA,SAAAA,CAAS,KAAK;GAK5C,MAAM,CAAC,KAAK,WAAA,GAAUA,MAAAA,SAAAA,CAAyD,IAAI;GACnF,MAAM,UAAA,GAASG,MAAAA,OAAAA,CAA8B,IAAI;GACjD,MAAM,WAAA,GAAUA,MAAAA,OAAAA,CAA8B,IAAI;GAGlD,MAAM,eAAA,GAAcA,MAAAA,OAAAA,CAAO,KAAK;GAMhC,MAAM,kBAAA,GAAiBA,MAAAA,OAAAA,CAAwC,IAAI;GAGnE,MAAM,gBAAA,GAAeA,MAAAA,OAAAA,CAA+B,IAAI;GACxD,CAAA,GAAA,MAAA,UAAA,aAAsB,aAAa,SAAS,MAAM,GAAG,CAAC,CAAC;GAMvD,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,MAAM,KAAK,OAAO;IAClB,IAAI,OAAO,MAAM;IACjB,MAAM,UAAU,MAAoB;KAAE,eAAe,UAAU;MAAE,GAAG,EAAE;MAAS,GAAG,EAAE;KAAQ;IAAE;IAC9F,GAAG,iBAAiB,eAAe,QAAQ,EAAE,SAAS,KAAK,CAAC;IAC5D,aAAa,GAAG,oBAAoB,eAAe,MAAM;GAC3D,GAAG,CAAC,CAAC;;GAGL,MAAM,eAAe,UAAkB;IACrC,IAAI,QAAQ,GAAG;IACf,MAAM,KAAK,OAAO,SAAS,cAA2B,oBAAoB,MAAM,GAAG;IACnF,IAAI,OAAO,QAAQ,OAAO,KAAA,GAAW;IACrC,MAAM,OAAO,GAAG,sBAAsB;IACtC,OAAO;KAAE;KAAO,GAAG,KAAK,OAAO;KAAI,GAAG,KAAK;IAAI,CAAC;GAClD;;;;GAKA,MAAM,mBAAmB,UAAkB;IACzC,IAAI,CAAC,YAAY,SAAS;KACxB,QAAQ,IAAI;KACZ;IACF;IACA,YAAY,KAAK;GACnB;GAKA,MAAM,oBAAA,GAAmBA,MAAAA,OAAAA,CAA6C,IAAI;GAC1E,MAAM,uBAAuB;IAC3B,IAAI,iBAAiB,YAAY,MAAM;KACrC,aAAa,iBAAiB,OAAO;KACrC,iBAAiB,UAAU;IAC7B;GACF;GACA,MAAM,mBAAmB,UAAkB;IACzC,eAAe;IACf,iBAAiB,UAAU,iBAAiB;KAC1C,iBAAiB,UAAU;KAC3B,QAAQ,SAAU,MAAM,UAAU,QAAQ,OAAO,IAAK;IACxD,GAAG,GAAG;GACR;GACA,CAAA,GAAA,MAAA,UAAA,OAAgB,gBAAgB,CAAC,CAAC;GAKlC,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,CAAC,MAAM;KACT,YAAY,UAAU;KACtB;IACF;IACA,MAAM,KAAK,OAAO;IAClB,IAAI,OAAO,MAAM;IACjB,IAAI,UAAU;IACd,MAAM,eAAe;KACnB,IAAI,SAAS;KACb,UAAU;KACV,YAAY,UAAU;KACtB,GAAG,oBAAoB,iBAAiB,eAAe;KACvD,aAAa,KAAK;KAKlB,MAAM,IAAI,eAAe;KACzB,IAAI,MAAM,MAAM;MACd,MAAM,MAAM,SAAS,iBAAiB,EAAE,GAAG,EAAE,CAAC;MAC9C,MAAM,OAAO,QAAQ,OAAO,OAAO,IAAI,QAAQ,kBAAkB;MACjE,MAAM,MAAM,SAAS,OAAO,KAAK,OAAO,KAAK,aAAa,gBAAgB,CAAC;MAC3E,IAAI,OAAO,UAAU,GAAG,KAAK,OAAO,GAAG,YAAY,GAAG;KACxD;IACF;IACA,MAAM,mBAAmB,MAAuB;KAC9C,IAAI,EAAE,iBAAiB,SAAS;KAChC,OAAO;IACT;IACA,GAAG,iBAAiB,iBAAiB,eAAe;IAIpD,MAAM,QAAQ,WAAW,QAAQ,GAAG;IACpC,aAAa;KACX,GAAG,oBAAoB,iBAAiB,eAAe;KACvD,aAAa,KAAK;IACpB;GACF,GAAG,CAAC,IAAI,CAAC;GAIT,MAAM,aAAA,GAAYC,MAAAA,QAAAA,OAA4C;IAC5D,IAAI,cAAc,KAAA,GAAW,OAAO,KAAA;IACpC,MAAM,IAAK,gBAAgB,QAAQ,SAAS,CAAC,EACuB;IACpE,OAAO,GAAG,mBAAmB,KAAA,IACzB,KAAA,KACC,OAAe,EAAE,eAAgB,EAAE;GAC1C,GAAG,CAAC,WAAW,eAAe,CAAC;GAM/B,MAAM,eAAA,GAAcC,MAAAA,YAAAA,EAAa,cAAsB;IACrD,MAAM,IAAI,SAAS,MAAM,cAAc,oBAAoB,SAAS,MAAM,SAAS;IACnF,IAAI,MAAM,KAAA,KAAa,iBAAiB,KAAA,KAAa,cAAc,KAAA,GAAW;IAC9E,MAAM,OAAO,WAAW,GAAG,YAAY,KAAK,EAAE;IAC9C,IAAI,aAAa,aAAa,KAAA,GAAW,aAAa,SAAS,IAAI;IACnE,MAAM,QAAQ,YAAY,GAAG,YAAY,CAAC,CAAC,QAAQ,SAA8C,KAAK,SAAS,KAAK;IACpH,IAAI,MAAM,WAAW,KAAK,iBAAiB,KAAA,KAAa,cAAc,KAAA,GAAW;IACjF,CAAM,YAAY;KAChB,MAAM,QAAgB,CAAC;KACvB,KAAK,MAAM,QAAQ,OACjB,IAAI;MACF,MAAM,SAAS,MAAM,UAAU,KAAK,YAAY;MAChD,IAAI,QAAQ,OAAO,QAAQ,OAAO,OAAO,SAAS,KAAA,GAAW;MAC7D,MAAM,YAAY,OAAO,MAAM,YAAY,aAAa,KAAK;MAC7D,MAAM,KAAK,IAAI,KAAK,CAAC,OAAO,MAAM,IAAgB,GAAG,cAAc,KAAK,eAAe,UAAU,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE,MAAM,UAAU,CAAC,CAAC;KAC7J,QAAQ,CAER;KAEF,IAAI,MAAM,WAAW,GAAG;KAExB,MAAM,MADS,aAAa,kBAAkB,KAC7B,CAAC,CAAC,KAAK,UAAU,MAAM,EAAE,CAAC,CAAC,QAAQ,OAAqB,OAAO,KAAA,CAAS;KACzF,IAAI,IAAI,SAAS,GACf,IAAI;MAAE,aAAa,YAAY,GAAG;KAAE,QAAQ,CAAkE;IAElH,EAAA,CAAG;GACL,GAAG;IAAC;IAAc;IAAc;IAAU;IAAc;IAAW;GAAS,CAAC;GAK7E,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,UAAU,YAAY;IACtB,UAAU,oBAAoB,cAAc;KAC1C,IAAI,cAAc,KAAA,GAAW,eAAe,WAAW,SAAS;IAClE;IACA,UAAU,SAAS;IACnB,0BAA0B,CAAC;IAC3B,aAAa;KAEX,IAAI,UAAU,cAAc,WAAW,UAAU,YAAY,KAAA;IAC/D;GACF,GAAG;IAAC;IAAW;IAAa;GAAC,CAAC;GAI9B,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,0BAA0B,CAAC;GAC7B,GAAG,CAAC,WAAW,CAAC,CAAC;GAIjB,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,OAAO,aAAa,eAAe,SAAS,SAAS,MAAM;IAC/D,IAAI,cAAc;IAClB,IAAI,OAAO,aAAa,aACtB,OAAO,oBAAoB,CAAC;GAEhC,GAAG,CAAC,cAAc,CAAC,CAAC;GAIpB,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,YAAY,KAAA,GAAW;IAC3B,IAAI,MAAM,QAAQ,WAAW,QAAQ,KAAK,UAAU,SAAS,SAAS,GAAG;IACzE,IAAI,YAAY;IAChB,MAAM,MAAM,YAAY;KACtB,IAAI,QAAQ;KACZ,OAAO,CAAC,aAAa,UAAU,KAAK;MAClC,IAAI,MAAM,QAAQ,WAAW,QAAQ,KAAK,UAAU,SAAS,SAAS,GAAG;MACzE,MAAM,OAAO,QAAQ,YAAY;MACjC,IAAI,MAAM,YAAY,MAAM;MAC5B,IAAI,KAAK,iBAAiB,MAAM;OAAE,MAAM,MAAM,EAAE;OAAG;MAAS;MAC5D,MAAO,QAAyD,UAAU;KAC5E;IACF;IACA,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC;IACpB,aAAa;KAAE,YAAY;IAAK;GAElC,GAAG;IAAC;IAAW,YAAY,KAAA,IAAY,SAAS;IAAS,MAAM,QAAQ,WAAW,QAAQ,KAAK,UAAU,SAAS,SAAS,IAAI,SAAS;GAAM,CAAC;GAG/I,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,kBAAkB,WAAW,GAAG;IACpC,MAAM,6BAAa,IAAI,IAAoB;IAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,kBAAkB,QAAQ,KAAK;KACjD,MAAM,MAAM,YAAY,kBAAkB,EAAE;KAC5C,IAAI,QAAQ,KAAA,GAAW,WAAW,IAAI,KAAK,CAAC;IAC9C;IACA,MAAM,qBAAqB;KACzB,MAAM,KAAK,SAAS,cAAc,4BAA4B;KAC9D,IAAI,OAAO,MAAM;KACjB,MAAM,OAAO,GAAG,sBAAsB;KACtC,IAAI,KAAK,WAAW,GAAG;KACvB,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS;KAGtC,MAAM,OAAO,GAAG,iBAAiB,gFAA4E;KAC7G,IAAI,OAAO;KACX,IAAI,WAAW;KACf,KAAK,MAAM,OAAO,MAAM;MACtB,MAAM,MAAM,IAAI,aAAa,sBAAsB;MACnD,IAAI,QAAQ,MAAM;MAClB,MAAM,MAAM,WAAW,IAAI,GAAG,KAAK;MACnC,IAAI,QAAQ,IAAI;MAChB,MAAM,IAAI,IAAI,sBAAsB;MACpC,MAAM,OAAO,KAAK,IAAI,EAAE,MAAM,EAAE,SAAS,IAAI,IAAI;MACjD,IAAI,OAAO,UAAU;OAAE,WAAW;OAAM,OAAO;MAAI;KACrD;KACA,eAAe,IAAI;IACrB;IACA,aAAa;IACb,MAAM,KAAK,SAAS,cAAc,4BAA4B;IAC9D,IAAI,cAAoD;IACxD,MAAM,iBAAiB;KACrB,IAAI,gBAAgB,MAAM;KAC1B,cAAc,iBAAiB;MAAE,cAAc;MAAM,aAAa;KAAE,GAAG,EAAE;IAC3E;IACA,IAAI,iBAAiB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;IAC1D,MAAM,QAAQ,OAAO,YAAY,cAAc,GAAI;IACnD,aAAa;KACX,IAAI,oBAAoB,UAAU,QAAQ;KAC1C,OAAO,cAAc,KAAK;KAC1B,IAAI,gBAAgB,MAAM,aAAa,WAAW;IACpD;GAEF,GAAG,CAAC,WAAW,kBAAkB,MAAM,CAAC;GAKxC,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,cAAc,GAAG;IAErB,MAAM,KAAK,QAAQ;IACnB,IAAI,OAAO,MAAM;IACjB,MAAM,OAAO,GAAG,cAA2B,oBAAoB,YAAY,GAAG;IAC9E,IAAI,SAAS,MAAM;IACnB,MAAM,SAAS,KAAK,YAAY,GAAG,eAAe,IAAI,KAAK,eAAe;IAC1E,GAAG,SAAS;KAAE,KAAK,KAAK,IAAI,GAAG,MAAM;KAAG,UAAU;IAAS,CAAC;GAC9D,GAAG,CAAC,WAAW,CAAC;GAMhB,IAAI,cAAc,KAAA,KAAa,SAAS,SAAS,GAAG,OAAO;GAI3D,MAAM,WAAW,QAAQ,OACrB,KACA,UACE,SAAS,WAAW,MAAM,kBAAkB,IAAI,WAAW,KAAA,KACxD,oBAAoB,CAAC,MAAM,oBAAoB,kBAAkB,IAAI,MAAM,CAAC,IAC/E,IAAI;GAEV,MAAM,QAAQ,kBAAkB,KAAK,GAAG,MAAM;IAC5C,MAAM,MAAM,YAAY,CAAC;IACzB,MAAM,UAAU,oBAAoB,CAAC,MAAM,MAAM,WAAW,WAAW,WAAW,oBAAoB,CAAC,CAAC;IACxG,QAAA,GAAOJ,MAAAA,cAAAA,CAAc,UAAU;KAC7B,MAAM;KACN,KAAK,EAAE;KACP,kBAAkB,OAAO,CAAC;KAE1B,GAAI,QAAQ,KAAA,IAAY,EAAE,mBAAmB,IAAI,IAAI,CAAC;KACtD,GAAI,EAAE,OAAO,OAAO,EAAE,eAAe,OAAO,IAAI,CAAC;KACjD,WAAW,EAAE,QAAQ,gBAAgB,IAAI,IAAI,EAAE,eAAe,OAAO,UAAU,IAAI,EAAE,YAAY;KACjG,cAAc,GAAG,EAAE,SAAS,IAAI,EAAE,KAAK,MAAM,GAAG,EAAE,KAAK,EAAE,OAAO,IAAI,EAAE,SAAS;KAC/E,gBAAgB,gBAAgB,IAAI,aAAa,KAAA;KACjD,UAAU;KACV,eAAe;MACb,IAAI,QAAQ,KAAA,KAAa,SAAS;MAClC,aAAa,SAAS,MAAM;MAC5B,MAAM,aAAa,IAAI,gBAAgB;MACvC,aAAa,UAAU;MACvB,cACE,iBACA,WAGA,UAAU,CAAC,IACV,MAAM,WAAW,cAAc,YAAY,GAAG,CAAC,GAChD,KAAA,GACA,WAAW,QAIX,EAAE,MAKD,UAAU,WAAW,UAAU,QAAQ,CAC1C,CAAC,CAAC,cAAc,WAAW,KAAK,CAAC;KACnC;KACA,oBAAoB,gBAAgB,CAAC;KACrC,oBAAoB,gBAAgB,CAAC;KACrC,UAAU;OACRA,GAAAA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,EAAE,IAAI,GAAG,IAAI,IAAI,GAAG;MACvD,WAAA,GACIA,MAAAA,cAAAA,CAAc,QAAQ;OAAE,WAAW,EAAE;OAAS,MAAM;OAAO,cAAc,EAAE;OAAK,eAAe,KAAA;MAAU,IAAA,GACzGA,MAAAA,cAAAA,CAAc,OAAO;OACnB,SAAS;OACT,OAAO;OACP,QAAQ;OACR,MAAM;OACN,QAAQ;OACR,eAAe;MACjB,IAAA,GACAA,MAAAA,cAAAA,CAAc,QAAQ;OAAE,GAAG;OAAW,eAAe;MAAK,CAAC,CAAC,CAAC,IAC7D;OACJA,GAAAA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,EAAE,MAAM,GAAG,EAAE,SAAS,KAAK,EAAE,SAAS,EAAE,IAAI;MAC/E,EAAE,YAAA,GACEA,MAAAA,cAAAA,CAAc,QAAQ;OAAE,WAAW,EAAE;OAAK,MAAM;OAAO,cAAc,EAAE;MAAS,IAAA,GAChFA,MAAAA,cAAAA,CAAc,OAAO;OACnB,SAAS;OACT,OAAO;OACP,QAAQ;OACR,MAAM;OACN,QAAQ;OACR,aAAa;OACb,eAAe;OACf,gBAAgB;OAChB,eAAe;MACjB,IAAA,GACAA,MAAAA,cAAAA,CAAc,QAAQ;OAAE,GAAG;OAAK,GAAG;OAAK,OAAO;OAAM,QAAQ;OAAM,IAAI;MAAI,CAAC,IAAA,GAC5EA,MAAAA,cAAAA,CAAc,UAAU;OAAE,IAAI;OAAK,IAAI;OAAK,GAAG;OAAM,MAAM;OAAgB,QAAQ;MAAO,CAAC,IAAA,GAC3FA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,GAAG,4CAA4C,CAAC,CAAC,CAAC,IAC1E;OACJA,GAAAA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,EAAE,KAAK,GAAG,aAAa,EAAE,MAAM,CAAC,CAAC;OACpEA,GAAAA,MAAAA,cAAAA,CAAc,QAAQ;OAAE,WAAW,EAAE;OAAK,eAAe;MAAK,IAAA,GAC5DA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC;KAChD;IACF,CAAC;GACH,CAAC;GAED,QAAA,GAAOK,UAAAA,aAAAA,CAIL,EAAA,GAACL,MAAAA,cAAAA,CAAc,OAAO;IACpB,KAAK;IACL,WAAW,EAAE,OAAO,OAAO,IAAI,EAAE,YAAY,OAAO,aAAa,IAAI,qBAAqB;IAE1F,GAAI,WAAW,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC;IACvE,MAAM;IACN,cAAc,EAAE;IAChB,oBAAoB,QAAQ,IAAI;IAChC,oBAAoB,QAAQ,KAAK;IACjC,UAAU;KAOR,iBAAA,GACIA,MAAAA,cAAAA,CAAc,UAAU;MACtB,MAAM;MACN,KAAK;MACL,WAAW,EAAE,aAAa,UAAU,YAAY;MAChD,gBAAgB;MAChB,cAAc,EAAE;MAChB,OAAO,EAAE;MACT,eAAe,MAAkB,EAAE,gBAAgB;MACnD,eAAe,YAAY,MAAM,CAAC,CAAC;MACnC,UAAU,EAAA,GACRA,MAAAA,cAAAA,CAAc,OAAO;OACnB,SAAS;OACT,OAAO;OACP,QAAQ;OACR,MAAM,UAAU,iBAAiB;OACjC,QAAQ;OACR,aAAa;OACb,gBAAgB;OAChB,eAAe;MACjB,IAAA,GAAGA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,GAAG,UAAU,CAAC,CAAC,IAAA,GAC1CA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,EAAE,eAAe,GAAG,EAAE,OAAO,CAClE;KACF,CAAC,IACD;KACJ,WAAA,GAAUA,MAAAA,cAAAA,CAAc,OAAO;MAAE,WAAW,EAAE;MAAS,KAAK;KAAU,IAAA,GACpEA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,EAAE,aAAa,GAAG,EAAE,OAAO,CAAC,IAAI;MAGrEA,GAAAA,MAAAA,cAAAA,CAAc,OAAO;MAAE,KAAK;MAAS,KAAK;MAAQ,WAAW,EAAE;KAAK,GAAG,GAAG,KAAK;IACjF;GACF,CAAC,GAID,QAAQ,QAAQ,YAAY,KAAK,WAAW,SAAS,UAAA,GACjDA,MAAAA,cAAAA,CAAc,OAAO;IACnB,WAAW,EAAE;IACb,OAAO;KAAE,MAAM,GAAG,IAAI,EAAE;KAAK,KAAK,GAAG,IAAI,EAAE;KAAK,WAAW;IAAoB;IAE/E,oBAAoB,eAAe;IACnC,oBAAoB,OAAO,IAAI;GACjC,UAAU;IACR,MAAM,IAAI,SAAS;IACnB,MAAM,QAAQ,YAAY,GAAG,YAAY;IACzC,MAAM,WAAwB,CAAC;IAC/B,IAAI,MAAM,SAAS,KAAK,cAAc,KAAA,GAIpC,SAAS,MAAA,GAAKA,MAAAA,cAAAA,CAAc,OAAO;KAAE,WAAW,EAAE;KAAS,KAAK;IAAO,IAAA,GACrEA,MAAAA,cAAAA,CAAc,OAAO;KAAE,WAAW,EAAE;KAAY,KAAK;IAAM,IAAA,GACzDA,MAAAA,cAAAA,CAAc,UAAU;KAAE,MAAM,MAAM;KAAe;KAAqB,MAAM;IAAU,CAAC,GAC3F,MAAM,SAAS,KAAA,GACXA,MAAAA,cAAAA,CAAc,QAAQ;KAAE,WAAW,EAAE;KAAa,eAAe;IAAK,GAAG,IAAI,MAAM,SAAS,GAAG,IAC/F,IAAI,CAAC,CAAC;SACT,IAAI,EAAE,UACX,SAAS,MAAA,GAAKA,MAAAA,cAAAA,CAAc,QAAQ;KAAE,WAAW,EAAE;KAAU,KAAK;IAAQ,GAAG,EAAE,QAAQ,CAAC;IAE1F,SAAS,MAAA,GAAKA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,KAAK,OAAO,GAAG,WAAW,GAAG,YAAY,KAAK,EAAE,MAAM,CAAC;IAG7F,MAAM,WAAW,gBAAgB,YAAY,UAAU,QAAQ;IAC/D,IAAI,SAAS,SAAS,GACpB,SAAS,MAAA,GAAKA,MAAAA,cAAAA,CAAc,OAAO;KAAE,WAAW,EAAE;KAAW,KAAK;IAAK,GACrE,SAAS,KAAK,OAAA,GAAMA,MAAAA,cAAAA,CAAc,UAAU;KAC1C,MAAM;KACN,KAAK,EAAE,OAAO,EAAE;KAChB,WAAW,EAAE;KACb,OAAO,EAAE;KACT,UAAU,MAAkB;MAC1B,EAAE,gBAAgB;MAClB,IAAI,SAAS;MACb,aAAa,SAAS,MAAM;MAC5B,MAAM,aAAa,IAAI,gBAAgB;MACvC,aAAa,UAAU;MACvB,cACE,iBACA,WACA,UAAU,CAAC,IACV,MAAM,WAAW,cAAc,YAAY,GAAG,CAAC,GAChD,KAAA,GACA,WAAW,QACX,EAAE,MACD,UAAU,WAAW,UAAU,QAAQ,CAC1C,CAAC,CAAC,cAAc,WAAW,KAAK,CAAC;KACnC;KACA,UAAU,EAAA,GAGRA,MAAAA,cAAAA,CAAc,OAAO;MACnB,KAAK;MACL,WAAW,EAAE;MACb,SAAS;MACT,OAAO;MACP,QAAQ;MACR,MAAM;MACN,QAAQ;MACR,aAAa;MACb,eAAe;MACf,gBAAgB;MAChB,eAAe;KACjB,IAAA,GACAA,MAAAA,cAAAA,CAAc,UAAU;MAAE,IAAI;MAAG,IAAI;MAAG,GAAG;MAAK,eAAe;KAAK,CAAC,IAAA,GACrEA,MAAAA,cAAAA,CAAc,QAAQ;MAAE,GAAG;MAA4C,eAAe;KAAK,CAAC,IAAA,GAC5FA,MAAAA,cAAAA,CAAc,QAAQ;MAAE,GAAG;MAAgB,eAAe;KAAK,CAAC,CAAC,IAAA,GACjEA,MAAAA,cAAAA,CAAc,QAAQ;MAAE,WAAW,EAAE;MAAW,KAAK;KAAM,GAAG,EAAE,IAAI,CACtE;IACF,CAAC,CAAC,CAAC,CAAC;IAER,OAAO;GACT,EAAA,CAAG,CAAC,IACJ,IAAI,GACR,SAAS,IACX;EACF;EAEA,SAAS,MAAM,KAA0B;GAMvC,IAAI;GACJ,IAAI,OAAO,CAAC,gBAAgB,IAAI,UAAyB;IACvD,MAAM,KAAM,MAA+D;IAC3E,UAAU,cAAkD;KAC1D,IAAI;MAEF,OADgB,GAAG,QAAQ,SACd,CAAC,CAAC,OAAO,MAAM;KAC9B,QAAQ;MAEN;KACF;IACF;GACF,CAAC;GACD,IAAI,MAAM,OAAO,iCAAiC,IAAI,MAAM,SAAS;IACnE,MAAM;IACN,IAAI;IACJ,OAAO;IACP,eAAe;KACb,iBAAiB,IAAI;KACrB;KAIA,cAAc,IAAI;IACpB;GACF,GAAG,YAAY,CAAC;GAQhB,qBADqB,IAAI,YAAY,IAAkB,WACvB,CAAC;GACjC,IAAI,MAAM,OAAO,+BAA+B,IAAI,MAAM,SAAS;IACjE,MAAM;IACN,KAAK;IACL,eAAe;KACb,2BAA2B,SAAkB,kBAAkB,IAAI,yBAAyB,IAAI;KAChG,GAAG,YAAY;IACjB;GACF,GAAY,mBAAmB,CAAC;EAClC;EAQA,SAAgB,oBAAoB,EAAE,0BAA0B,KAAyC;GACvG,MAAM,CAAC,MAAM,YAAA,GAAWD,MAAAA,SAAAA,CAAS,KAAK;GAKtC,MAAM,MAAA,GAJOE,MAAAA,qBAAAA,CACX,qBAAqB,QAAQ,OAAmB,iBAAkB,UAAU,EAAE,IAAI,gBAC5E,kBAAkB,YAAY,KAAK,EAAE,OAAO,KAAA,EAAU,CAEhD,CAAC,CAAC,OAAO,0BAA0B;GACjD,QAAA,GAAOD,MAAAA,cAAAA,CAAc,MAAM,EAAE,WAAW,gBAAgB,OAAO,oBAAoB,IAAI,IAAA,GACrFA,MAAAA,cAAAA,CAAc,UAAU;IACtB,MAAM;IACN,WAAW;IACX,iBAAiB;IACjB,eAAe,SAAS,MAAM,CAAC,CAAC;IAChC,UAAU,EAAA,GACRA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,iBAAiB,IAAA,GAClDA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,aAAa,GAAG,EAAE,QAAQ,IAAA,GAC7DA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,aAAa,GAAG,EAAE,QAAQ,CAC/D,IAAA,GACAA,MAAAA,cAAAA,CAAc,OAAO;KACnB,WAAW,mBAAmB,OAAO,qBAAqB;KAC1D,SAAS;KAAa,OAAO;KAAI,QAAQ;KACzC,MAAM;KAAQ,eAAe;IAC/B,IAAA,GAAGA,MAAAA,cAAAA,CAAc,QAAQ;KACvB,GAAG;KACH,MAAM;IACR,CAAC,CAAC,CACJ;GACF,CAAC,GACD,QAAA,GAAOA,MAAAA,cAAAA,CAAc,OAAO;IAAE,WAAW;IAAc,UAAU,EAAA,GAC/DA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,YAAY,GAAG,EAAA,GAC/CA,MAAAA,cAAAA,CAAc,OAAO,EAAE,WAAW,iBAAiB,IAAA,GACjDA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,aAAa,GAAG,EAAE,eAAe,CACtE,IAAA,GACAA,MAAAA,cAAAA,CAAc,UAAU;KACtB,MAAM;KACN,MAAM;KACN,gBAAgB;KAChB,WAAW,kBAAkB,KAAK,QAAQ;KAC1C,eAAe,yBAAyB,CAAC,EAAE;KAC3C,WAAA,GAAUA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,aAAa,CAAC;IAC7D,CAAC,CACH,CAAC,CACH;GAAE,CAAC,IAAI,IACT;EACF"}
|