@max-null/dsh-chat-rail 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useSyncExternalStore","useState","useRef","createElement","createPortal"],"sources":["../src/client/index.tsx"],"sourcesContent":["/**\r\n * @max-null/dsh-chat-rail — web client half.\r\n *\r\n * Right-edge conversation navigation rail: one indicator per user message,\r\n * scroll-spy highlight of the reading position, hover preview, click to jump.\r\n *\r\n * Two deliberate fixes over the dsh-chat-timeline reference:\r\n *\r\n * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout\r\n * push through the `--dsh-sidebar-width` CSS variable on `:root` (the\r\n * `#root` margin-right transition reads the same variable). The rail's\r\n * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same\r\n * transition timing, so rail and scrollport move together while the panel\r\n * expands — no \"panel first, scrollbar later\" lag.\r\n *\r\n * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)\r\n * lines — nearly invisible on a light background. Here the rail keeps a\r\n * constant translucent capsule, thicker/darker indicator lines, and an\r\n * always-visible collapsed state (no \"reveal on pointer proximity\").\r\n *\r\n * Data: host `chatRail` projection first, loaded chat nodes fallback, then a\r\n * background loadOlder loop (stopped as soon as the projection delivers).\r\n * Mounted in conversation.input.dock, portal-rendered to body.\r\n */\r\n\r\nimport { createElement, useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\r\nimport type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'\r\n// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry).\r\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\r\n\r\nexport const inject = ['slots', 'sessions']\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 },\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 },\r\n}\r\n\r\n/** Compact relative time for a message timestamp (zh/en via template). */\r\nfunction relativeTime(ts: number, s: Record<string, string>): string {\r\n const diff = Date.now() - ts\r\n if (diff < 60_000) return s.timeJustNow\r\n if (diff < 3_600_000) return s.timeMinutes.replace('{n}', String(Math.floor(diff / 60_000)))\r\n if (diff < 86_400_000) return s.timeHours.replace('{n}', String(Math.floor(diff / 3_600_000)))\r\n if (diff < 7 * 86_400_000) return s.timeDays.replace('{n}', String(Math.floor(diff / 86_400_000)))\r\n const d = new Date(ts)\r\n return `${d.getMonth() + 1}/${d.getDate()}`\r\n}\r\n\r\n// ---- styles ----\r\n// 画卷式导航:单个容器,折叠态 36px 竖条;hover 时宽度从右往左展开到 280px\r\n// (打开画卷效果)。容器垂直方向在「去掉底栏后的可用区域」内居中:\r\n// top = (100vh - 底栏高)/2 + translateY(-50%),底栏收起时正中、展开时上移避让。\r\n// right 跟随 --dsh-sidebar-width、top 跟随 --dsh-sidebar-height,与 better-sidebar\r\n// 面板共享同一 CSS 变量 + transition,动画同步。\r\nconst css = [\r\n '.crl_nav{user-select:none;z-index:100;position:fixed;right:calc(var(--dsh-sidebar-width,0px) + 3px);top:calc((100vh - var(--dsh-sidebar-height,0px)) / 2);transform:translateY(-50%);width:36px;max-height:min(60vh,420px,calc(100vh - var(--dsh-sidebar-height,0px) - 40px));display:flex;flex-direction:column;align-items:center;box-sizing:border-box;padding:10px 0;border-radius:18px;overflow-y:hidden;overflow-x:hidden;background:rgba(255,255,255,.55);border:1px solid rgba(0,0,0,.07);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.18) transparent;transition:width .25s cubic-bezier(.4,0,.2,1),right var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),top var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),background .2s ease,border-color .2s ease,box-shadow .2s ease}',\r\n 'body[data-ds-dark-theme] .crl_nav,[data-theme=\\'dark\\'] .crl_nav,.dark .crl_nav{background:rgba(28,28,32,.6);border-color:rgba(255,255,255,.09);scrollbar-color:rgba(255,255,255,.25) transparent}',\r\n '.crl_nav.crl_show{width:280px;overflow-y:auto;align-items:stretch;background:rgba(255,255,255,.94);border-color:rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(0,0,0,.10),0 2px 8px rgba(0,0,0,.05)}',\r\n 'body[data-ds-dark-theme] .crl_nav.crl_show,[data-theme=\\'dark\\'] .crl_nav.crl_show,.dark .crl_nav.crl_show{background:rgba(28,28,32,.96);border-color:rgba(255,255,255,.09);box-shadow:0 10px 30px rgba(0,0,0,.5),0 2px 8px rgba(0,0,0,.28)}',\r\n '.crl_nav::-webkit-scrollbar{width:4px}',\r\n '.crl_nav::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);border-radius:4px}',\r\n // Jump-in-progress indicator: sticky row pinned at the rail top. The spinner\r\n // icon is always visible; the \"Loading…\" label only appears once expanded\r\n // (in the collapsed 36px rail the text would overflow the capsule).\r\n '.crl_loading{position:sticky;top:0;z-index:2;flex-shrink:0;width:100%;padding:4px 0;font-size:10px;line-height:1;text-align:center;color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.5)));background:inherit;pointer-events:none}',\r\n '.crl_loading::before{content:\"\";display:inline-block;width:8px;height:8px;margin-right:4px;border:1.5px solid var(--dsw-alias-state-business-primary,#4d6bfe);border-top-color:transparent;border-radius:50%;vertical-align:-1px;animation:crl-spin .8s linear infinite}',\r\n '.crl_loading .crl_loadingLabel{display:none}',\r\n '.crl_show .crl_loading .crl_loadingLabel{display:inline}',\r\n '@keyframes crl-spin{to{transform:rotate(360deg)}}',\r\n // Item row: always flex-end (the indicator rides the right edge through the\r\n // width animation, never jumping). The collapsed padding (0 9px) centers the\r\n // 18px indicator in the 36px capsule; the expanded padding (0 14px) clears\r\n // the text. Padding transitions with the same timing as the width, so the\r\n // indicator glides smoothly instead of snapping when text hides/shows.\r\n '.crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:flex-end;align-items:center;width:100%;box-sizing:border-box;padding:0 9px;line-height:20px;background:none;border:none;font:inherit;text-align:left;color:rgba(0,0,0,.68);transition:padding .25s cubic-bezier(.4,0,.2,1),color .15s ease}',\r\n '.crl_show .crl_item{padding:0 14px}',\r\n '.crl_item:hover{color:rgba(0,0,0,.95)}',\r\n '.crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_item,[data-theme=\\'dark\\'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}',\r\n 'body[data-ds-dark-theme] .crl_item:hover,[data-theme=\\'dark\\'] .crl_item:hover,.dark .crl_item:hover{color:rgba(255,255,255,.95)}',\r\n // Title / ordinal / time: display:none in collapsed state (so they take no\r\n // flex space and the indicator line stays centered), revealed on expand.\r\n // The ordinal has a fixed width (right-aligned) so message text starts at\r\n // the same x across items; time sits AFTER the title (no fixed width, so it\r\n // never shrinks the text area).\r\n '.crl_title{display:none;font-size:13px;line-height:20px;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;flex:1;min-width:0;text-align:left;overflow:hidden;color:inherit}',\r\n '.crl_show .crl_title{display:block;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_active .crl_title{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:500}',\r\n '.crl_num{display:none;flex-shrink:0;width:28px;font-size:10px;line-height:20px;color:rgba(0,0,0,.35);margin-right:8px;text-align:right;user-select:none}',\r\n '.crl_show .crl_num{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_num,[data-theme=\\'dark\\'] .crl_num,.dark .crl_num{color:rgba(255,255,255,.35)}',\r\n '.crl_time{display:none;flex-shrink:0;font-size:10px;line-height:20px;color:rgba(0,0,0,.28);margin-right:10px;user-select:none;white-space:nowrap}',\r\n '.crl_show .crl_time{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_time,[data-theme=\\'dark\\'] .crl_time,.dark .crl_time{color:rgba(255,255,255,.28)}',\r\n '@keyframes crl-fade{from{opacity:0}to{opacity:1}}',\r\n // Indicator line: 10×3px, clearly visible in both themes.\r\n '.crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}',\r\n '.crl_show .crl_ind{margin-left:8px}',\r\n '.crl_img{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:12px;height:12px;margin-left:6px;border:1px solid currentColor;border-radius:2px;opacity:.65}',\r\n '.crl_img::before{content:\"\";width:6px;height:6px;border:1px solid currentColor;border-radius:1px;background:radial-gradient(circle at 30% 30%,currentColor 0 1px,transparent 1.5px)}',\r\n '.crl_line{background-color:rgba(0,0,0,.45);border-radius:4px;flex-shrink:0;width:10px;height:3px;transition:background-color .2s ease,transform .2s ease}',\r\n '.crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}',\r\n '.crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe);transform-origin:50%;transform:scale(1.4);box-shadow:0 0 6px var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_line,[data-theme=\\'dark\\'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}',\r\n 'body[data-ds-dark-theme] .crl_item:hover .crl_line,[data-theme=\\'dark\\'] .crl_item:hover .crl_line,.dark .crl_item:hover .crl_line{background-color:rgba(255,255,255,.95)}',\r\n 'body[data-ds-dark-theme] .crl_item.crl_active .crl_line,[data-theme=\\'dark\\'] .crl_item.crl_active .crl_line,.dark .crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n // Full-content hover panel: floats to the LEFT of the expanded rail list.\r\n '.crl_tip{position:fixed;z-index:200;max-width:360px;max-height:70vh;overflow-y:auto;padding:10px 12px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary,var(--text-primary,rgba(0,0,0,.85)));background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(255,255,255,.97)));border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)));border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16);white-space:pre-wrap;word-break:break-word;pointer-events:none}',\r\n 'body[data-ds-dark-theme] .crl_tip,[data-theme=\\'dark\\'] .crl_tip,.dark .crl_tip{background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(28,28,32,.97)));border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}',\r\n '@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}}',\r\n].join('')\r\n\r\nconst STYLE_ID = '@max-null/dsh-chat-rail/styles.module.css'\r\nif (typeof document !== 'undefined' && document.querySelector(`style[data-plugin-css=\"${STYLE_ID}\"]`) === null) {\r\n const tag = document.createElement('style')\r\n tag.dataset.plugin = 'dsh-chat-rail'\r\n tag.dataset.pluginCss = STYLE_ID\r\n tag.textContent = css\r\n document.head.appendChild(tag)\r\n}\r\n\r\nconst S = {\r\n nav: 'crl_nav',\r\n navShow: 'crl_show',\r\n item: 'crl_item',\r\n itemActive: 'crl_active',\r\n title: 'crl_title',\r\n num: 'crl_num',\r\n time: 'crl_time',\r\n ind: 'crl_ind',\r\n line: 'crl_line',\r\n img: 'crl_img',\r\n loading: 'crl_loading',\r\n loadingLabel: 'crl_loadingLabel',\r\n tip: 'crl_tip',\r\n}\r\n\r\n// ---- data helpers ----\r\nconst NOOP_STORE = { getSnapshot: () => undefined, subscribe: () => () => {} }\r\n\r\ninterface RailMessage {\r\n seq: number\r\n time: number\r\n text: string\r\n hasImage: boolean\r\n key?: string\r\n id?: string\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/** 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 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 ...(typeof o.key === 'string' ? { key: o.key } : {}),\r\n ...(typeof o.id === 'string' ? { id: o.id } : {}),\r\n }\r\n}\r\n\r\n/** Fallback collector: enumerate user messages from the loaded chat nodes. */\r\nfunction collectFromNodes(snapshot: unknown): RailMessage[] {\r\n const out: RailMessage[] = []\r\n if (snapshot === undefined || (snapshot as { chat?: unknown }).chat === undefined) return out\r\n const chat = (snapshot as { chat: { nodes?: Map<unknown, unknown> } }).chat\r\n if (!chat.nodes) return out\r\n for (const node of chat.nodes.values()) {\r\n if (node === null || typeof node !== 'object') continue\r\n const n = node as { kind?: unknown; key?: unknown; anchorSeq?: unknown; data?: unknown }\r\n if (n.kind !== 'user') continue\r\n const data = n.data as { time?: unknown; content?: unknown } | null\r\n if (data === null || typeof data !== 'object') continue\r\n if (typeof data.time !== 'number' || !Array.isArray(data.content)) continue\r\n const key = typeof n.key === 'string' ? n.key : undefined\r\n if (key === undefined) continue\r\n out.push({ seq: typeof n.anchorSeq === 'number' ? n.anchorSeq : 0, time: data.time, text: userTextOf(data.content), hasImage: userHasImage(data.content), key })\r\n }\r\n out.sort((a, b) => a.seq - b.seq)\r\n return out\r\n}\r\n\r\n/** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */\r\nfunction anchorKeyOf(m: RailMessage): string | undefined {\r\n if (typeof m.key === 'string' && m.key !== '') return m.key\r\n if (typeof m.id === 'string' && m.id !== '') return '13:input-message' + m.id\r\n return undefined\r\n}\r\n\r\nconst delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\r\n\r\n/**\r\n * Ensure the message node is loaded into the visible window, then scroll to it.\r\n * loadOlder pages 50 messages at a time (DSH PAGE_MESSAGES), so a jump far back\r\n * in a long session needs many pages. Report page count through onProgress so\r\n * the UI can show a loading state instead of appearing frozen.\r\n *\r\n * The paged-in data lands in the session snapshot synchronously with\r\n * loadOlder's resolution, but the chat rows render through React — the DOM row\r\n * for the target key appears a beat later. We therefore poll for the row (with\r\n * a timeout) instead of querying once and giving up; otherwise a multi-page\r\n * jump would \"finish loading\" without scrolling and need a second click.\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 onProgress?: (pages: number) => void,\r\n signal?: AbortSignal,\r\n): Promise<boolean> {\r\n const session = sessionsService.binding(sessionId)?.session\r\n if (session === undefined) return false\r\n let pages = 0\r\n let guard = 0\r\n let loaded = false\r\n while (guard++ < 120) {\r\n if (signal?.aborted) return false\r\n const snapshot = session.getSnapshot() as { chat?: { nodes?: Map<string, unknown> }; hasMore?: boolean; loadingOlder?: boolean } | undefined\r\n if (snapshot?.chat?.nodes?.get(key) !== undefined) {\r\n loaded = true\r\n break\r\n }\r\n if (snapshot?.hasMore !== true) break\r\n if (snapshot.loadingOlder === true) {\r\n // Another loader owns the current page; wait for it without busy-spinning.\r\n await delay(50)\r\n continue\r\n }\r\n await session.loadOlder()\r\n pages++\r\n // Report progress only on page boundaries, not every loop iteration.\r\n onProgress?.(pages)\r\n }\r\n // Page guard exhausted (or hasMore ran out) without the node materializing:\r\n // fail fast with context instead of burning the DOM poll below.\r\n if (!loaded) {\r\n console.warn(`[chat-rail] jumpToMessage: node \"${key}\" not loaded after ${pages} page(s)`)\r\n return false\r\n }\r\n const scrollport = typeof document !== 'undefined' ? document.querySelector('[data-conversation-scroll]') : null\r\n if (scrollport === null) return false\r\n // The snapshot is authoritative for \"loaded\", but the row's DOM node only\r\n // appears once React renders the prepended page. Poll for it (up to ~5s) so a\r\n // freshly paged-in jump scrolls on the same click.\r\n let row: Element | null = null\r\n let waited = 0\r\n while (waited++ < 100) {\r\n if (signal?.aborted) return false\r\n row = scrollport.querySelector(`[data-chat-anchor-key=\"${CSS.escape(key)}\"]`)\r\n if (row !== null) break\r\n await delay(50)\r\n }\r\n if (row === null) return false\r\n const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\r\n row.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'center' })\r\n return true\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}\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 }: 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 const fallbackStore = session === undefined ? NOOP_STORE : session\r\n const nodeSnapshot = useSyncExternalStore(\r\n (cb) => fallbackStore.subscribe(cb),\r\n () => fallbackStore.getSnapshot(),\r\n )\r\n\r\n let messages: RailMessage[] = []\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) {\r\n messages = projected.messages.map(normalize).filter((m): m is RailMessage => m !== null)\r\n }\r\n if (messages.length === 0) {\r\n messages = collectFromNodes(nodeSnapshot)\r\n }\r\n\r\n 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 // 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 // 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 /** Position the tip against the item's CURRENT (post-expand) rect. */\r\n const positionTip = (index: number) => {\r\n if (index < 0) return\r\n const el = navRef.current?.querySelector<HTMLElement>(`[data-crl-index=\"${index}\"]`)\r\n if (el === null || el === undefined) return\r\n const rect = el.getBoundingClientRect()\r\n setTip({ index, x: rect.left - 12, y: rect.top })\r\n }\r\n\r\n /** Handle item hover: the collapsed state only expands the rail; the tip\r\n * only triggers once the expand animation has settled (item rects are then\r\n * stable, so the tip is positioned correctly). */\r\n const handleItemEnter = (index: number) => {\r\n if (!expandedRef.current) {\r\n setShow(true)\r\n return\r\n }\r\n positionTip(index)\r\n }\r\n\r\n const handleItemLeave = (index: number) => {\r\n setTip((prev) => (prev?.index === index ? null : prev))\r\n }\r\n\r\n // Track the settled expand state: when `show` flips true, wait for the width\r\n // transition to end before allowing tip triggers; when it flips false, the\r\n // rail is collapsing — treat as unexpanded immediately.\r\n useEffect(() => {\r\n if (!show) {\r\n expandedRef.current = false\r\n return\r\n }\r\n const el = navRef.current\r\n if (el === null) return\r\n const onTransitionEnd = (e: TransitionEvent) => {\r\n if (e.propertyName !== 'width') return\r\n expandedRef.current = true\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n }\r\n el.addEventListener('transitionend', onTransitionEnd)\r\n return () => el.removeEventListener('transitionend', onTransitionEnd)\r\n }, [show])\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\n const fullTextOf = (m: RailMessage): string => {\r\n const key = anchorKeyOf(m)\r\n const nodes = (nodeSnapshot as { chat?: { nodes?: Map<string, { data?: { content?: unknown } }> } } | undefined)?.chat?.nodes\r\n const node = key === undefined ? undefined : nodes?.get(key)\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 // 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 (messages.length === 0) return\r\n const indexByKey = new Map<string, number>()\r\n for (let i = 0; i < messages.length; i++) {\r\n const key = anchorKeyOf(messages[i])\r\n if (key !== undefined) indexByKey.set(key, i)\r\n }\r\n const updateActive = () => {\r\n const sp = document.querySelector('[data-conversation-scroll]')\r\n if (sp === null) return\r\n const rect = sp.getBoundingClientRect()\r\n if (rect.height === 0) return\r\n const line = rect.top + rect.height * 0.4\r\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"]')\r\n let best = -1\r\n let bestDist = Infinity\r\n for (const row of rows) {\r\n const key = row.getAttribute('data-chat-anchor-key')\r\n if (key === null) continue\r\n const idx = indexByKey.get(key) ?? -1\r\n if (idx === -1) continue\r\n const r = row.getBoundingClientRect()\r\n const dist = Math.abs(r.top + r.height / 2 - line)\r\n if (dist < bestDist) { bestDist = dist; best = idx }\r\n }\r\n setActiveIndex(best)\r\n }\r\n updateActive()\r\n const el = document.querySelector('[data-conversation-scroll]')\r\n let scrollTimer: ReturnType<typeof setTimeout> | null = null\r\n const onScroll = () => {\r\n if (scrollTimer !== null) return\r\n scrollTimer = setTimeout(() => { scrollTimer = null; updateActive() }, 60)\r\n }\r\n el?.addEventListener('scroll', onScroll, { passive: true })\r\n const timer = window.setInterval(updateActive, 2000)\r\n return () => {\r\n el?.removeEventListener('scroll', onScroll)\r\n window.clearInterval(timer)\r\n if (scrollTimer !== null) clearTimeout(scrollTimer)\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, messages.length])\r\n\r\n // Scroll the rail so the active item stays centered in its visible area:\r\n // follows the conversation's reading position (and re-centers after a\r\n // session switch, where activeIndex is recomputed for the new session).\r\n useEffect(() => {\r\n if (activeIndex < 0) return\r\n const el = navRef.current\r\n if (el === null) return\r\n const item = el.querySelector<HTMLElement>(`[data-crl-index=\"${activeIndex}\"]`)\r\n if (item === null) return\r\n const target = item.offsetTop - el.clientHeight / 2 + item.clientHeight / 2\r\n el.scrollTo({ top: Math.max(0, target), behavior: 'smooth' })\r\n }, [activeIndex])\r\n\r\n if (sessionId === undefined || messages.length < 2) return null\r\n\r\n const items = messages.map((m, i) => {\r\n const key = anchorKeyOf(m)\r\n return createElement('button', {\r\n type: 'button',\r\n key: m.seq,\r\n 'data-crl-index': String(i),\r\n className: S.item + (activeIndex === i ? ` ${S.itemActive}` : ''),\r\n 'aria-label': `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,\r\n 'aria-current': activeIndex === i ? 'location' : undefined,\r\n disabled: jumping,\r\n onClick: () => {\r\n if (key === undefined || jumping) return\r\n jumpAbortRef.current?.abort()\r\n const controller = new AbortController()\r\n jumpAbortRef.current = controller\r\n setJumping(true)\r\n void jumpToMessage(sessionsService as never, sessionId as string, key, undefined, controller.signal)\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 createElement('span', { className: S.title }, m.text === '' ? t.noText : m.text),\r\n m.hasImage ? createElement('span', { className: S.img, title: t.hasImage, 'aria-label': t.hasImage }) : null,\r\n createElement('span', { className: S.time }, relativeTime(m.time, t)),\r\n createElement('span', { className: S.ind, 'aria-hidden': true },\r\n createElement('span', { className: S.line })),\r\n ],\r\n })\r\n })\r\n\r\n return createPortal(\r\n // Fragment: the nav (transformed) and the tip must be siblings — a fixed\r\n // child inside a transformed ancestor is positioned relative to that\r\n // ancestor, breaking the viewport coordinates the tip uses.\r\n [createElement('div', {\r\n ref: navRef,\r\n className: S.nav + (show ? ` ${S.navShow}` : ''),\r\n role: 'navigation',\r\n 'aria-label': t.railLabel,\r\n onMouseEnter: () => setShow(true),\r\n onMouseLeave: () => setShow(false),\r\n children: [\r\n jumping ? createElement('div', { className: S.loading, key: 'loading' },\r\n createElement('span', { className: S.loadingLabel }, t.loading)) : null,\r\n ...items,\r\n ],\r\n }),\r\n // Full-content panel: anchored to the left of the hovered item.\r\n tip !== null && tip.index >= 0 && tip.index < 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 }, fullTextOf(messages[tip.index]) || t.noText)\r\n : null],\r\n document.body,\r\n )\r\n}\r\n\r\nfunction apply(ctx: ClientContext): void {\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: () => ({ sessionsService: ctx.sessions }),\r\n }, TimelineRail))\r\n}\r\n\r\nexport { apply, TimelineRail }\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useState","createElement","useSyncExternalStore","useRef","useMemo","useCallback","createPortal"],"sources":["../src/client/index.tsx"],"sourcesContent":["/**\r\n * @max-null/dsh-chat-rail — web client half.\r\n *\r\n * Right-edge conversation navigation rail: one indicator per user message,\r\n * scroll-spy highlight of the reading position, hover preview, click to jump.\r\n *\r\n * Two deliberate fixes over the dsh-chat-timeline reference:\r\n *\r\n * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout\r\n * push through the `--dsh-sidebar-width` CSS variable on `:root` (the\r\n * `#root` margin-right transition reads the same variable). The rail's\r\n * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same\r\n * transition timing, so rail and scrollport move together while the panel\r\n * expands — no \"panel first, scrollbar later\" lag.\r\n *\r\n * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)\r\n * lines — nearly invisible on a light background. Here the rail keeps a\r\n * constant translucent capsule, thicker/darker indicator lines, and an\r\n * always-visible collapsed state (no \"reveal on pointer proximity\").\r\n *\r\n * Data: host `chatRail` projection first, loaded chat nodes fallback, then a\r\n * background loadOlder loop (stopped as soon as the projection delivers).\r\n * Mounted in conversation.input.dock, portal-rendered to body.\r\n */\r\n\r\nimport { createElement, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\r\nimport type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'\r\n// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry).\r\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\r\n\r\nexport const inject = ['slots', 'sessions', 'conversation']\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 },\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 },\r\n}\r\n\r\n/** Compact relative time for a message timestamp (zh/en via template). */\r\nfunction relativeTime(ts: number, s: Record<string, string>): string {\r\n const diff = Date.now() - ts\r\n if (diff < 60_000) return s.timeJustNow\r\n if (diff < 3_600_000) return s.timeMinutes.replace('{n}', String(Math.floor(diff / 60_000)))\r\n if (diff < 86_400_000) return s.timeHours.replace('{n}', String(Math.floor(diff / 3_600_000)))\r\n if (diff < 7 * 86_400_000) return s.timeDays.replace('{n}', String(Math.floor(diff / 86_400_000)))\r\n const d = new Date(ts)\r\n return `${d.getMonth() + 1}/${d.getDate()}`\r\n}\r\n\r\n// ---- styles ----\r\n// 画卷式导航:单个容器,折叠态 36px 竖条;hover 时宽度从右往左展开到 280px\r\n// (打开画卷效果)。容器垂直方向在「去掉底栏后的可用区域」内居中:\r\n// top = (100vh - 底栏高)/2 + translateY(-50%),底栏收起时正中、展开时上移避让。\r\n// right 跟随 --dsh-sidebar-width、top 跟随 --dsh-sidebar-height,与 better-sidebar\r\n// 面板共享同一 CSS 变量 + transition,动画同步。\r\nconst css = [\r\n '.crl_nav{user-select:none;z-index:100;position:fixed;right:calc(var(--dsh-sidebar-width,0px) + 3px);top:calc((100vh - var(--dsh-sidebar-height,0px)) / 2);transform:translateY(-50%);width:36px;max-height:min(60vh,420px,calc(100vh - var(--dsh-sidebar-height,0px) - 40px));display:flex;flex-direction:column;align-items:center;box-sizing:border-box;padding:10px 0;border-radius:18px;overflow-y:hidden;overflow-x:hidden;background:rgba(255,255,255,.55);border:1px solid rgba(0,0,0,.07);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.18) transparent;transition:width .25s cubic-bezier(.4,0,.2,1),right var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),top var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),background .2s ease,border-color .2s ease,box-shadow .2s ease}',\r\n 'body[data-ds-dark-theme] .crl_nav,[data-theme=\\'dark\\'] .crl_nav,.dark .crl_nav{background:rgba(28,28,32,.6);border-color:rgba(255,255,255,.09);scrollbar-color:rgba(255,255,255,.25) transparent}',\r\n '.crl_nav.crl_show{width:280px;overflow-y:auto;align-items:stretch;background:rgba(255,255,255,.94);border-color:rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(0,0,0,.10),0 2px 8px rgba(0,0,0,.05)}',\r\n 'body[data-ds-dark-theme] .crl_nav.crl_show,[data-theme=\\'dark\\'] .crl_nav.crl_show,.dark .crl_nav.crl_show{background:rgba(28,28,32,.96);border-color:rgba(255,255,255,.09);box-shadow:0 10px 30px rgba(0,0,0,.5),0 2px 8px rgba(0,0,0,.28)}',\r\n '.crl_nav::-webkit-scrollbar{width:4px}',\r\n '.crl_nav::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);border-radius:4px}',\r\n // Jump-in-progress indicator: sticky row pinned at the rail top. The spinner\r\n // icon is always visible; the \"Loading…\" label only appears once expanded\r\n // (in the collapsed 36px rail the text would overflow the capsule).\r\n '.crl_loading{position:sticky;top:0;z-index:2;flex-shrink:0;width:100%;padding:4px 0;font-size:10px;line-height:1;text-align:center;color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.5)));background:inherit;pointer-events:none}',\r\n '.crl_loading::before{content:\"\";display:inline-block;width:8px;height:8px;margin-right:4px;border:1.5px solid var(--dsw-alias-state-business-primary,#4d6bfe);border-top-color:transparent;border-radius:50%;vertical-align:-1px;animation:crl-spin .8s linear infinite}',\r\n '.crl_loading .crl_loadingLabel{display:none}',\r\n '.crl_show .crl_loading .crl_loadingLabel{display:inline}',\r\n '@keyframes crl-spin{to{transform:rotate(360deg)}}',\r\n // Item row: always flex-end (the indicator rides the right edge through the\r\n // width animation, never jumping). The collapsed padding (0 9px) centers the\r\n // 18px indicator in the 36px capsule; the expanded padding (0 14px) clears\r\n // the text. Padding transitions with the same timing as the width, so the\r\n // indicator glides smoothly instead of snapping when text hides/shows.\r\n '.crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:flex-end;align-items:center;width:100%;box-sizing:border-box;padding:0 9px;line-height:20px;background:none;border:none;font:inherit;text-align:left;color:rgba(0,0,0,.68);transition:padding .25s cubic-bezier(.4,0,.2,1),color .15s ease}',\r\n '.crl_show .crl_item{padding:0 14px}',\r\n '.crl_item:hover{color:rgba(0,0,0,.95)}',\r\n '.crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_item,[data-theme=\\'dark\\'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}',\r\n 'body[data-ds-dark-theme] .crl_item:hover,[data-theme=\\'dark\\'] .crl_item:hover,.dark .crl_item:hover{color:rgba(255,255,255,.95)}',\r\n // Title / ordinal / time: display:none in collapsed state (so they take no\r\n // flex space and the indicator line stays centered), revealed on expand.\r\n // The ordinal has a fixed width (right-aligned) so message text starts at\r\n // the same x across items; time sits AFTER the title (no fixed width, so it\r\n // never shrinks the text area).\r\n '.crl_title{display:none;font-size:13px;line-height:20px;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;flex:1;min-width:0;text-align:left;overflow:hidden;color:inherit}',\r\n '.crl_show .crl_title{display:block;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_active .crl_title{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:500}',\r\n '.crl_num{display:none;flex-shrink:0;width:28px;font-size:10px;line-height:20px;color:rgba(0,0,0,.35);margin-right:8px;text-align:right;user-select:none}',\r\n '.crl_show .crl_num{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_num,[data-theme=\\'dark\\'] .crl_num,.dark .crl_num{color:rgba(255,255,255,.35)}',\r\n '.crl_time{display:none;flex-shrink:0;font-size:10px;line-height:20px;color:rgba(0,0,0,.28);margin-right:10px;user-select:none;white-space:nowrap}',\r\n '.crl_show .crl_time{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_time,[data-theme=\\'dark\\'] .crl_time,.dark .crl_time{color:rgba(255,255,255,.28)}',\r\n '@keyframes crl-fade{from{opacity:0}to{opacity:1}}',\r\n // Indicator line: 10×3px, clearly visible in both themes.\r\n '.crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}',\r\n '.crl_show .crl_ind{margin-left:8px}',\r\n // Image badge: hidden in the collapsed state (the rail is a clean 36px\r\n // capsule of indicator lines; an icon there would crowd and distract) and\r\n // revealed with the other labels on expand. A quiet stroke glyph in a muted\r\n // tone that ignores the active-blue text color.\r\n '.crl_img{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-left:6px;color:rgba(0,0,0,.38)}',\r\n '.crl_show .crl_img{display:inline-flex;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_img,[data-theme=\\'dark\\'] .crl_img,.dark .crl_img{color:rgba(255,255,255,.38)}',\r\n '.crl_line{background-color:rgba(0,0,0,.45);border-radius:4px;flex-shrink:0;width:10px;height:3px;transition:background-color .2s ease,transform .2s ease}',\r\n '.crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}',\r\n '.crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe);transform-origin:50%;transform:scale(1.4);box-shadow:0 0 6px var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_line,[data-theme=\\'dark\\'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}',\r\n 'body[data-ds-dark-theme] .crl_item:hover .crl_line,[data-theme=\\'dark\\'] .crl_item:hover .crl_line,.dark .crl_item:hover .crl_line{background-color:rgba(255,255,255,.95)}',\r\n 'body[data-ds-dark-theme] .crl_item.crl_active .crl_line,[data-theme=\\'dark\\'] .crl_item.crl_active .crl_line,.dark .crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n // Full-content hover panel: floats to the LEFT of the expanded rail list.\r\n '.crl_tip{position:fixed;z-index:200;max-width:360px;max-height:70vh;overflow-y:auto;padding:10px 12px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary,var(--text-primary,rgba(0,0,0,.85)));background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(255,255,255,.97)));border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)));border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16);white-space:pre-wrap;word-break:break-word;pointer-events:none}',\r\n 'body[data-ds-dark-theme] .crl_tip,[data-theme=\\'dark\\'] .crl_tip,.dark .crl_tip{background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(28,28,32,.97)));border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}',\r\n // Tip thumbnail gallery: stacked above the text, one row per image, capped\r\n // height so a tall screenshot cannot swallow the preview panel. The\r\n // placeholder pulses while session.readAttachment resolves the bytes; a\r\n // failed load removes the row entirely rather than showing a broken image.\r\n '.crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}',\r\n '.crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}',\r\n '.crl_tipImg{display:block;max-width:100%;max-height:150px;width:auto;height:auto;object-fit:contain;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)))}',\r\n '.crl_tipImgCount{position:absolute;right:6px;bottom:6px;padding:0 6px;font-size:10px;line-height:16px;border-radius:9px;background:rgba(0,0,0,.55);color:rgba(255,255,255,.95);pointer-events:none}',\r\n '.crl_tipImgPh{flex-shrink:0;height:56px;border-radius:8px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.05)));animation:crl-pulse 1.2s ease-in-out infinite}',\r\n 'body[data-ds-dark-theme] .crl_tipImg,[data-theme=\\'dark\\'] .crl_tipImg,.dark .crl_tipImg{border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}',\r\n '.crl_tipBadge{display:inline-block;margin-bottom:8px;padding:0 7px;font-size:10px;line-height:16px;border-radius:9px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.06)));color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.45)))}',\r\n '@keyframes crl-pulse{0%,100%{opacity:.45}50%{opacity:.85}}',\r\n // Message action buttons injected next to the copy button (DOM row: the\r\n // user-message IconActions row). The host rows use 28px round icon-action\r\n // buttons (padding 6, 16px glyphs, DSW token colors, hover fill) — the\r\n // injected buttons mirror the exact geometry and token surface so the star\r\n // and plus read as native siblings of copy/branch. The star lights\r\n // yellowish (#ffd166, milestone-compatible) once favorited.\r\n // Tooltip: same dark pill as the host Tooltip (black rounded label, no\r\n // native title balloon). The delay mirrors the host tooltip's settle; the\r\n // fade is short so the pill appears right beside the hovered button.\r\n '.crl_msgAct{position:relative;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:0;padding:6px;border:none;border-radius:28px;background:transparent;color:var(--dsw-alias-label-tertiary,rgba(0,0,0,.42));cursor:pointer;transition:background .15s ease,color .15s ease}',\r\n '.crl_msgAct:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06));color:var(--dsw-alias-label-secondary,rgba(0,0,0,.72))}',\r\n '.crl_msgAct:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4d6bfe);outline-offset:2px}',\r\n '.crl_msgAct[data-tip]:hover::after{content:attr(data-tip);position:absolute;left:50%;bottom:calc(100% + 8px);transform:translateX(-50%);padding:5px 10px;border-radius:8px;background:rgba(24,28,36,.94);color:rgba(255,255,255,.95);font-size:12px;line-height:1.4;white-space:nowrap;pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}',\r\n '.crl_msgAct[data-tip]:hover::before{content:\"\";position:absolute;left:50%;bottom:calc(100% + 3px);transform:translateX(-50%);border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid rgba(24,28,36,.94);pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}',\r\n '@keyframes crl-tip-in{to{opacity:1}}',\r\n '.crl_msgAct.crl_fav.crl_on{color:#ffd166}',\r\n '.crl_msgAct.crl_fav.crl_on svg{fill:currentColor}',\r\n '.crl_msgAct.crl_fav.crl_on:hover{color:#ffd166}',\r\n // Rail row favorite badge + yellow indicator line for favorited messages.\r\n '.crl_favStar{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-right:6px;color:#ffd166}',\r\n '.crl_show .crl_favStar{display:inline-flex;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_favItem .crl_line{background-color:#ffd166}',\r\n 'body[data-ds-dark-theme] .crl_item.crl_favItem .crl_line,[data-theme=\\'dark\\'] .crl_item.crl_favItem .crl_line,.dark .crl_item.crl_favItem .crl_line{background-color:#ffd166}',\r\n // A favorited line that is also the current reading position keeps its\r\n // yellow identity — the generic active glow is blue, which pairs miserably\r\n // with the gold line, so the favorite-active state recasts the glow in the\r\n // same warm tone.\r\n '.crl_item.crl_favItem.crl_active .crl_line{background-color:#ffd166;box-shadow:0 0 6px #ffd166}',\r\n // Rail-top favorites-only toggle: a round icon pill in the collapsed state;\r\n // on expand it becomes a full-width label row (star + \"bookmarks only\") so\r\n // the control reads as a real rail header action instead of a stray dot.\r\n // STICKY: the expanded rail's rows scroll inside the capsule (overflow-y),\r\n // and the pill must stay pinned at the top instead of scrolling away with\r\n // them — sticky + an opaque inherited background keeps the labels reading\r\n // cleanly over passing rows.\r\n '.crl_favToggle{position:sticky;top:0;z-index:3;flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:6px;width:26px;height:26px;margin:0 0 6px;padding:0;border:none;border-radius:13px;background:inherit;color:rgba(0,0,0,.4);cursor:pointer;font-size:11px;line-height:1;white-space:nowrap;transition:background .15s ease,color .15s ease}',\r\n '.crl_favToggle:hover{background:rgba(0,0,0,.07);color:rgba(0,0,0,.75)}',\r\n '.crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.14)}',\r\n '.crl_show .crl_favToggle{width:auto;height:26px;margin:0 6px 8px;padding:0 11px;border-radius:13px;align-self:flex-start;text-align:left}',\r\n '.crl_favToggleLabel{display:none}',\r\n '.crl_show .crl_favToggleLabel{display:inline-block;min-width:0;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}',\r\n 'body[data-ds-dark-theme] .crl_favToggle,[data-theme=\\'dark\\'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4)}',\r\n 'body[data-ds-dark-theme] .crl_favToggle:hover,[data-theme=\\'dark\\'] .crl_favToggle:hover,.dark .crl_favToggle:hover{background:rgba(255,255,255,.1);color:rgba(255,255,255,.85)}',\r\n 'body[data-ds-dark-theme] .crl_favToggle.crl_on,[data-theme=\\'dark\\'] .crl_favToggle.crl_on,.dark .crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.18)}',\r\n '@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}',\r\n].join('')\r\n\r\nconst STYLE_ID = '@max-null/dsh-chat-rail/styles.module.css'\r\nif (typeof document !== 'undefined' && document.querySelector(`style[data-plugin-css=\"${STYLE_ID}\"]`) === null) {\r\n const tag = document.createElement('style')\r\n tag.dataset.plugin = 'dsh-chat-rail'\r\n tag.dataset.pluginCss = STYLE_ID\r\n tag.textContent = css\r\n document.head.appendChild(tag)\r\n}\r\n\r\nconst S = {\r\n nav: 'crl_nav',\r\n navShow: 'crl_show',\r\n item: 'crl_item',\r\n itemActive: 'crl_active',\r\n title: 'crl_title',\r\n num: 'crl_num',\r\n time: 'crl_time',\r\n ind: 'crl_ind',\r\n line: 'crl_line',\r\n img: 'crl_img',\r\n loading: 'crl_loading',\r\n loadingLabel: 'crl_loadingLabel',\r\n tip: 'crl_tip',\r\n tipImgs: 'crl_tipImgs',\r\n tipImgWrap: 'crl_tipImgWrap',\r\n tipImg: 'crl_tipImg',\r\n tipImgCount: 'crl_tipImgCount',\r\n tipImgPh: 'crl_tipImgPh',\r\n tipBadge: 'crl_tipBadge',\r\n favStar: 'crl_favStar',\r\n favItem: 'crl_favItem',\r\n favToggle: 'crl_favToggle',\r\n favToggleLabel: 'crl_favToggleLabel',\r\n}\r\n\r\n// ---- data helpers ----\r\nconst NOOP_STORE = { getSnapshot: () => undefined, subscribe: () => () => {} }\r\n\r\n// ---- favorites (persisted per-session bookmark store) ----\r\n// Shape: Record<sessionId, messageId[]>. The durable message id is the same\r\n// identity the host projection emits (`id`) and the DOM anchor encodes\r\n// (`13:input-message<id>`), so DOM-injected buttons and the rail agree on\r\n// the key with no projection hop.\r\n// 持久化(2026-08-27):host 文件(~/.dsh/chat-rail-favorites.json,经\r\n// /chat-rail/api/favorites GET/PUT)——localStorage 按 origin 隔离,思灵 DSH\r\n// web 端口每次启动随机 → 跨重启丢弃。localStorage 仅作旧数据一次性迁移。\r\nconst FAVORITES_KEY = '@max-null/dsh-chat-rail:favorites'\r\ntype FavoritesMap = Record<string, string[]>\r\n\r\nconst FAVORITES_API = '/chat-rail/api/favorites'\r\n/** 模块级快照缓存(host 加载后充当源 truth)+ 订阅者集合。 */\r\nlet favoritesCache: FavoritesMap | null = null\r\nconst favoritesListeners = new Set<() => void>()\r\n\r\n/** 从 localStorage 读旧数据(迁移用/离线兜底)。 */\r\nfunction readFavoritesLocal(): FavoritesMap {\r\n if (typeof localStorage === 'undefined') return {}\r\n try {\r\n const raw = localStorage.getItem(FAVORITES_KEY)\r\n if (raw === null) return {}\r\n const value: unknown = JSON.parse(raw)\r\n return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as FavoritesMap : {}\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\nasync function fetchFavoritesMap(): Promise<FavoritesMap> {\r\n try {\r\n const res = await fetch(FAVORITES_API, { headers: { Accept: 'application/json' } })\r\n if (!res.ok) return {}\r\n const data = await res.json() as { ok?: boolean, value?: unknown }\r\n const value = data?.value\r\n return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as FavoritesMap : {}\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\n/** 模块加载时同步 host;旧 localStorage 数据迁移一次(host 为空且本地有值)。 */\r\nvoid (async () => {\r\n const host = await fetchFavoritesMap()\r\n if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {\r\n const legacy = readFavoritesLocal()\r\n if (Object.keys(host).length === 0 && Object.keys(legacy).length > 0) {\r\n await fetch(FAVORITES_API, {\r\n method: 'PUT', headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ favorites: legacy }),\r\n }).catch(() => {})\r\n }\r\n try { localStorage.removeItem(FAVORITES_KEY) } catch { /* 忽略 */ }\r\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\r\n favoritesCache = Object.keys(host).length === 0 ? legacy : host\r\n // host 就位后通知快照订阅(rail/收藏按钮重渲染)\r\n for (const listener of favoritesListeners) listener()\r\n } else {\r\n favoritesCache = host\r\n }\r\n})()\r\n\r\n/** Read the favorites map(内存镜像;未加载完成时回退 localStorage)。 */\r\nexport function readFavorites(): FavoritesMap {\r\n if (favoritesCache !== null) return favoritesCache\r\n return readFavoritesLocal()\r\n}\r\n\r\n/** Write the favorites map;更新内存并 fire-and-forget 持久化到 host(host-only)。 */\r\nfunction writeFavorites(map: FavoritesMap): void {\r\n favoritesCache = map\r\n void fetch(FAVORITES_API, {\r\n method: 'PUT', headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ favorites: map }),\r\n }).catch(() => { /* 网络失败:内存仍在用(下次写会重试) */ })\r\n}\r\n\r\n/** Normalized id list for one session (drops non-string entries). */\r\nexport function favoriteIdsOf(map: FavoritesMap, sessionId: string): string[] {\r\n const list = map[sessionId]\r\n return Array.isArray(list) ? list.filter((id): id is string => typeof id === 'string') : []\r\n}\r\n\r\n/** Toggle one id in a list (stable new array; no in-place mutation). */\r\nexport function toggleFavoriteId(list: string[], id: string): string[] {\r\n return list.includes(id) ? list.filter((candidate) => candidate !== id) : [...list, id]\r\n}\r\n\r\n/** Whether one message is favorited in one session. */\r\nexport function isFavorite(map: FavoritesMap, sessionId: string, messageId: string): boolean {\r\n return favoriteIdsOf(map, sessionId).includes(messageId)\r\n}\r\n\r\n/** Stable snapshot for useSyncExternalStore (loaded once, cached until toggle). */\r\nfunction favoritesSnapshot(): FavoritesMap {\r\n if (favoritesCache === null) favoritesCache = readFavorites()\r\n return favoritesCache\r\n}\r\n\r\n/** Subscribe to favorites changes; returns the unsubscribe. */\r\nfunction subscribeFavorites(listener: () => void): () => void {\r\n favoritesListeners.add(listener)\r\n return () => { favoritesListeners.delete(listener) }\r\n}\r\n\r\n/** Toggle one message's favorite state: snapshot → new map → persist → notify. */\r\nfunction toggleFavorite(sessionId: string, messageId: string): void {\r\n const next = { ...favoritesSnapshot() }\r\n next[sessionId] = toggleFavoriteId(favoriteIdsOf(favoritesSnapshot(), sessionId), messageId)\r\n favoritesCache = next\r\n writeFavorites(next)\r\n for (const listener of [...favoritesListeners]) listener()\r\n}\r\n\r\n/** Remember the messageId read off a DOM anchor key (`13:input-message<id>`). */\r\nexport function messageIdOfAnchorKey(key: string): string {\r\n return key.startsWith('13:input-message') ? key.slice('13:input-message'.length) : key\r\n}\r\n\r\n/** The favorite key of one rail message: the durable id when present (host\r\n * projection), otherwise the id reconstructed from the node anchor (the\r\n * loaded-node fallback path). */\r\nfunction favoriteIdOfMessage(m: Pick<RailMessage, 'id' | 'key'>): string {\r\n if (typeof m.id === 'string' && m.id !== '') return m.id\r\n if (typeof m.key === 'string') return messageIdOfAnchorKey(m.key)\r\n return ''\r\n}\r\n\r\n// ---- message-action injector (DOM) ----\r\n// User-message rows have no official action slot (assistant-actions serves\r\n// assistant turns only), so the star (favorite) and plus (fill-to-input)\r\n// buttons are injected next to the copy button of each user row. The rows\r\n// are located by their anchor key so history paging and re-renders both\r\n// land; the injector is a MutationObserver that re-scans after any DOM\r\n// change and never double-injects (row marker).\r\n//\r\n// Runtime wiring lives in a module-level context the rail component updates\r\n// every render (session id + handlers), so the pure DOM buttons stay tiny:\r\n// they only read data attributes and call context handlers.\r\nconst actionCtx: {\r\n sessionId: string | undefined\r\n onToggleFavorite: (messageId: string) => void\r\n onFill: (messageId: string) => void\r\n} = {\r\n sessionId: undefined,\r\n onToggleFavorite: () => {},\r\n onFill: () => {},\r\n}\r\n\r\n/** aria-label values of the copy button across shipped locales (button sits\r\n * in the user row's IconActions; its parent is the row's action host). */\r\nconst COPY_ARIA_LABELS = new Set(['复制', 'Copy', '已复制', 'Copied'])\r\n\r\n/** The copy button inside one user message row (null until rendered). */\r\nfunction copyButtonOf(row: HTMLElement): HTMLButtonElement | null {\r\n for (const button of row.querySelectorAll<HTMLButtonElement>('button')) {\r\n const label = button.getAttribute('aria-label')\r\n if (label !== null && COPY_ARIA_LABELS.has(label)) return button\r\n }\r\n return null\r\n}\r\n\r\n/** Star SVG path (milestone-compatible glyph, 24-unit viewBox). */\r\nconst STAR_PATH = 'm12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'\r\n\r\n/** Build the favorite (star) button for one user message row. */\r\nfunction favoriteButtonOf(messageId: string, lang: Record<string, string>): HTMLButtonElement {\r\n const button = document.createElement('button')\r\n button.type = 'button'\r\n button.className = 'crl_msgAct crl_fav'\r\n button.dataset.crlStar = messageId\r\n const initialLabel = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? lang.unfav : lang.fav\r\n button.setAttribute('aria-pressed', String(isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId)))\r\n button.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? 'true' : undefined\r\n button.classList.toggle('crl_on', isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId))\r\n // The CSS tooltip reads data-tip (a short dark pill like the host Tooltip);\r\n // the native title balloon is intentionally absent — double tips would\r\n // stack a browser bubble over the styled one.\r\n button.setAttribute('data-tip', initialLabel)\r\n button.setAttribute('aria-label', initialLabel)\r\n button.innerHTML = `<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"${STAR_PATH}\"/></svg>`\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault()\r\n event.stopPropagation()\r\n // Read the id off the dataset at click time: the injector may refresh the\r\n // button for a new session and the closure must stay current.\r\n const id = button.dataset.crlStar\r\n if (id !== undefined) {\r\n actionCtx.onToggleFavorite(id)\r\n refreshFavoriteButton(button, lang)\r\n }\r\n })\r\n return button\r\n}\r\n\r\n/** Build the fill-to-input (plus) button for one user message row. */\r\nfunction fillButtonOf(messageId: string, lang: Record<string, string>): HTMLButtonElement {\r\n const button = document.createElement('button')\r\n button.type = 'button'\r\n button.className = 'crl_msgAct crl_fill'\r\n button.dataset.crlFill = messageId\r\n button.setAttribute('data-tip', lang.fill)\r\n button.setAttribute('aria-label', lang.fill)\r\n button.innerHTML = '<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" aria-hidden=\"true\"><path d=\"M8 3.5v9M3.5 8h9\"/></svg>'\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault()\r\n event.stopPropagation()\r\n const id = button.dataset.crlFill\r\n if (id !== undefined) actionCtx.onFill(id)\r\n })\r\n return button\r\n}\r\n\r\n/** Sync one star button to its message's current favorite state. */\r\nfunction refreshFavoriteButton(button: HTMLButtonElement, lang: Record<string, string>): void {\r\n const id = button.dataset.crlStar\r\n if (id === undefined) return\r\n const starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', id)\r\n button.setAttribute('aria-pressed', String(starred))\r\n button.dataset.starred = starred ? 'true' : undefined\r\n button.classList.toggle('crl_on', starred)\r\n const label = starred ? lang.unfav : lang.fav\r\n // Both the a11y label and the styled tooltip follow the new state.\r\n button.setAttribute('data-tip', label)\r\n button.setAttribute('aria-label', label)\r\n}\r\n\r\n/** Refresh every injected star button (session switch / external toggle). */\r\nfunction refreshAllFavoriteButtons(lang: Record<string, string>): void {\r\n for (const button of document.querySelectorAll<HTMLButtonElement>('[data-crl-star]')) {\r\n refreshFavoriteButton(button, lang)\r\n }\r\n}\r\n\r\n/** Inject star + plus buttons into one user message row (idempotent). */\r\nfunction injectMessageActions(row: HTMLElement, lang: Record<string, string>): void {\r\n // Already injected and both buttons still present → leave it alone.\r\n if (row.dataset.crlActions === '1') {\r\n if (row.querySelector('[data-crl-star]') !== null && row.querySelector('[data-crl-fill]') !== null) return\r\n // React re-rendered the row and replaced the icons: fall through to re-inject.\r\n }\r\n const copy = copyButtonOf(row)\r\n const messageId = messageIdOfAnchorKey(row.getAttribute('data-chat-anchor-key') ?? '')\r\n if (copy === null || messageId === '') return\r\n const star = favoriteButtonOf(messageId, lang)\r\n star.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? 'true' : undefined\r\n star.classList.toggle('crl_on', isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId))\r\n const fill = fillButtonOf(messageId, lang)\r\n copy.before(star, fill)\r\n row.dataset.crlActions = '1'\r\n}\r\n\r\n/** Start observing new user rows so injected actions follow history paging. */\r\nfunction startActionInjector(lang: Record<string, string>): () => void {\r\n const scan = () => {\r\n for (const row of document.querySelectorAll<HTMLElement>('[data-chat-anchor-key^=\"13:input-message\"]')) {\r\n injectMessageActions(row, lang)\r\n }\r\n }\r\n const observer = new MutationObserver(scan)\r\n if (typeof document !== 'undefined' && document.body !== null) {\r\n observer.observe(document.body, { childList: true, subtree: true })\r\n }\r\n scan()\r\n return () => observer.disconnect()\r\n}\r\n\r\ninterface RailImage {\r\n attachmentId: string\r\n mediaType: string\r\n width: number\r\n height: number\r\n}\r\n\r\ninterface RailMessage {\r\n seq: number\r\n time: number\r\n text: string\r\n hasImage: boolean\r\n /** Stored-image references from the host projection (empty when inline-only). */\r\n images?: RailImage[]\r\n key?: string\r\n id?: string\r\n}\r\n\r\n/** One displayable tip image: either a durable reference (resolved lazily) or\r\n * an inline base64 payload already usable as an <img> src. */\r\ntype ImageSpec =\r\n | { kind: 'ref'; attachmentId: string; mediaType: string }\r\n | { kind: 'data'; src: string }\r\n\r\n/** Extract stored-image references from a ContentBlock list (reference form). */\r\nfunction nodeImagesOf(content: unknown): RailImage[] {\r\n if (!Array.isArray(content)) return []\r\n const out: RailImage[] = []\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a === null || typeof a !== 'object') continue\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown; width?: unknown; height?: unknown }\r\n if (typeof ref.attachmentId !== 'string' || ref.attachmentId === '') continue\r\n out.push({\r\n attachmentId: ref.attachmentId,\r\n mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png',\r\n width: typeof ref.width === 'number' ? ref.width : 0,\r\n height: typeof ref.height === 'number' ? ref.height : 0,\r\n })\r\n }\r\n return out\r\n}\r\n\r\n/** Convert a ContentBlock list to displayable image specs; inline base64\r\n * blocks (rare in replayed history) become data URLs on the spot. */\r\nfunction imageSpecsOfContent(content: unknown): ImageSpec[] {\r\n const specs: ImageSpec[] = []\r\n if (!Array.isArray(content)) return specs\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown; data?: unknown; mediaType?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a !== null && typeof a === 'object') {\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown }\r\n if (typeof ref.attachmentId === 'string' && ref.attachmentId !== '') {\r\n specs.push({ kind: 'ref', attachmentId: ref.attachmentId, mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png' })\r\n continue\r\n }\r\n }\r\n if (typeof b.data === 'string' && b.data !== '') {\r\n specs.push({ kind: 'data', src: `data:${typeof b.mediaType === 'string' ? b.mediaType : 'image/png'};base64,${b.data}` })\r\n }\r\n }\r\n return specs\r\n}\r\n\r\n/** Normalize one projection entry to a rail message. */\r\nfunction normalize(m: unknown): RailMessage | null {\r\n if (m === null || typeof m !== 'object') return null\r\n const o = m as Record<string, unknown>\r\n if (typeof o.seq !== 'number') return null\r\n // Host projection emits `text`; keep `preview` as a defensive fallback in\r\n // case an older projection payload is still cached in the browser.\r\n const text = typeof o.text === 'string' ? o.text : typeof o.preview === 'string' ? o.preview : ''\r\n const images = Array.isArray(o.images)\r\n ? o.images.map(nodeImagesOfEntry).filter((i): i is RailImage => i !== null)\r\n : undefined\r\n return {\r\n seq: o.seq,\r\n time: typeof o.time === 'number' ? o.time : 0,\r\n text,\r\n // Older projections lack hasImage; defensive default false.\r\n hasImage: o.hasImage === true,\r\n ...(images !== undefined && images.length > 0 ? { images } : {}),\r\n ...(typeof o.key === 'string' ? { key: o.key } : {}),\r\n ...(typeof o.id === 'string' ? { id: o.id } : {}),\r\n }\r\n}\r\n\r\n/** Normalize one projection entry of the `images` array (wire form). */\r\nfunction nodeImagesOfEntry(v: unknown): RailImage | null {\r\n if (v === null || typeof v !== 'object') return null\r\n const o = v as Record<string, unknown>\r\n if (typeof o.attachmentId !== 'string' || o.attachmentId === '') return null\r\n return {\r\n attachmentId: o.attachmentId,\r\n mediaType: typeof o.mediaType === 'string' ? o.mediaType : 'image/png',\r\n width: typeof o.width === 'number' ? o.width : 0,\r\n height: typeof o.height === 'number' ? o.height : 0,\r\n }\r\n}\r\n\r\n/** Extract preview text from a user message's ContentBlock list. */\r\nfunction userTextOf(content: unknown): string {\r\n if (!Array.isArray(content)) return ''\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n return out.trim().slice(0, 80)\r\n}\r\n\r\n/** Whether a ContentBlock list carries an image block (rc.8 attachments). */\r\nfunction userHasImage(content: unknown): boolean {\r\n if (!Array.isArray(content)) return false\r\n return content.some(block => block !== null && typeof block === 'object'\r\n && (block as { type?: unknown }).type === 'image')\r\n}\r\n\r\n/** Fallback collector: enumerate user messages from the loaded chat nodes. */\r\nfunction collectFromNodes(snapshot: unknown): RailMessage[] {\r\n const out: RailMessage[] = []\r\n if (snapshot === undefined || (snapshot as { chat?: unknown }).chat === undefined) return out\r\n const chat = (snapshot as { chat: { nodes?: Map<unknown, unknown> } }).chat\r\n if (!chat.nodes) return out\r\n for (const node of chat.nodes.values()) {\r\n if (node === null || typeof node !== 'object') continue\r\n const n = node as { kind?: unknown; key?: unknown; anchorSeq?: unknown; data?: unknown }\r\n if (n.kind !== 'user') continue\r\n const data = n.data as { time?: unknown; content?: unknown } | null\r\n if (data === null || typeof data !== 'object') continue\r\n if (typeof data.time !== 'number' || !Array.isArray(data.content)) continue\r\n const key = typeof n.key === 'string' ? n.key : undefined\r\n if (key === undefined) continue\r\n const images = nodeImagesOf(data.content)\r\n out.push({\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 out.sort((a, b) => a.seq - b.seq)\r\n return out\r\n}\r\n\r\n/** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */\r\nfunction anchorKeyOf(m: RailMessage): string | undefined {\r\n if (typeof m.key === 'string' && m.key !== '') return m.key\r\n if (typeof m.id === 'string' && m.id !== '') return '13:input-message' + m.id\r\n return undefined\r\n}\r\n\r\n/** Full text of a rail message from the loaded chat nodes (uncapped),\r\n * falling back to the projection preview when the node is not mounted. */\r\nfunction fullTextOf(m: RailMessage, nodeSnapshot: unknown): string {\r\n const key = anchorKeyOf(m)\r\n const nodes = (nodeSnapshot as { chat?: { nodes?: Map<string, { data?: { content?: unknown } }> } } | undefined)?.chat?.nodes\r\n const node = key === undefined ? undefined : nodes?.get(key)\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 nodes = (nodeSnapshot as { chat?: { nodes?: Map<string, { data?: { content?: unknown } }> } } | undefined)?.chat?.nodes\r\n const node = key === undefined ? undefined : nodes?.get(key)\r\n return imageSpecsOfContent(node?.data?.content)\r\n}\r\n\r\nconst delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\r\n\r\n/**\r\n * Ensure the message node is loaded into the visible window, then scroll to it.\r\n * loadOlder pages 50 messages at a time (DSH PAGE_MESSAGES), so a jump far back\r\n * in a long session needs many pages. Report page count through onProgress so\r\n * the UI can show a loading state instead of appearing frozen.\r\n *\r\n * The paged-in data lands in the session snapshot synchronously with\r\n * loadOlder's resolution, but the chat rows render through React — the DOM row\r\n * for the target key appears a beat later. We therefore poll for the row (with\r\n * a timeout) instead of querying once and giving up; otherwise a multi-page\r\n * jump would \"finish loading\" without scrolling and need a second click.\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 onProgress?: (pages: number) => void,\r\n signal?: AbortSignal,\r\n): Promise<boolean> {\r\n const session = sessionsService.binding(sessionId)?.session\r\n if (session === undefined) return false\r\n let pages = 0\r\n let guard = 0\r\n let loaded = false\r\n while (guard++ < 120) {\r\n if (signal?.aborted) return false\r\n const snapshot = session.getSnapshot() as { chat?: { nodes?: Map<string, unknown> }; hasMore?: boolean; loadingOlder?: boolean } | undefined\r\n if (snapshot?.chat?.nodes?.get(key) !== undefined) {\r\n loaded = true\r\n break\r\n }\r\n if (snapshot?.hasMore !== true) break\r\n if (snapshot.loadingOlder === true) {\r\n // Another loader owns the current page; wait for it without busy-spinning.\r\n await delay(50)\r\n continue\r\n }\r\n await session.loadOlder()\r\n pages++\r\n // Report progress only on page boundaries, not every loop iteration.\r\n onProgress?.(pages)\r\n }\r\n // Page guard exhausted (or hasMore ran out) without the node materializing:\r\n // fail fast with context instead of burning the DOM poll below.\r\n if (!loaded) {\r\n console.warn(`[chat-rail] jumpToMessage: node \"${key}\" not loaded after ${pages} page(s)`)\r\n return false\r\n }\r\n const scrollport = typeof document !== 'undefined' ? document.querySelector('[data-conversation-scroll]') : null\r\n if (scrollport === null) return false\r\n // The snapshot is authoritative for \"loaded\", but the row's DOM node only\r\n // appears once React renders the prepended page. Poll for it (up to ~5s) so a\r\n // freshly paged-in jump scrolls on the same click.\r\n let row: Element | null = null\r\n let waited = 0\r\n while (waited++ < 100) {\r\n if (signal?.aborted) return false\r\n row = scrollport.querySelector(`[data-chat-anchor-key=\"${CSS.escape(key)}\"]`)\r\n if (row !== null) break\r\n await delay(50)\r\n }\r\n if (row === null) return false\r\n const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\r\n row.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'center' })\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 /** 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, 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 const fallbackStore = session === undefined ? NOOP_STORE : session\r\n const nodeSnapshot = useSyncExternalStore(\r\n (cb) => fallbackStore.subscribe(cb),\r\n () => fallbackStore.getSnapshot(),\r\n )\r\n\r\n let messages: RailMessage[] = []\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) {\r\n messages = projected.messages.map(normalize).filter((m): m is RailMessage => m !== null)\r\n }\r\n if (messages.length === 0) {\r\n messages = collectFromNodes(nodeSnapshot)\r\n }\r\n\r\n // Favorites: per-session persisted set + a \"bookmarks only\" filter that\r\n // narrows the rail to favorited messages (mirrors dsh-milestone's\r\n // bookmarksOnly toggle; the DOM-injected buttons share the same store).\r\n const favorites = useSyncExternalStore(subscribeFavorites, favoritesSnapshot)\r\n const [favOnly, setFavOnly] = useState(false)\r\n // Count of favorited messages in THIS session. The filter toggle is only\r\n // worth showing when a favorite exists — with zero favorites there is\r\n // nothing to filter to, so the pill disappears entirely.\r\n const favCount = sessionId === undefined\r\n ? 0\r\n : messages.filter((m) => isFavorite(favorites, sessionId, favoriteIdOfMessage(m))).length\r\n const showFavToggle = favCount > 0\r\n const effectiveMessages = favOnly\r\n ? messages.filter((m) => sessionId !== undefined && isFavorite(favorites, sessionId, favoriteIdOfMessage(m)))\r\n : messages\r\n\r\n // If the filter is armed and the last favorite is un-starred (or the\r\n // session switched), the filtered list would be empty AND the toggle would\r\n // vanish — leaving a rail with nothing to un-filter. Auto-release the\r\n // filter instead so the full list returns.\r\n useEffect(() => {\r\n if (favOnly && !showFavToggle) setFavOnly(false)\r\n }, [favOnly, showFavToggle])\r\n\r\n const [activeIndex, setActiveIndex] = useState(-1)\r\n const [show, setShow] = useState(false)\r\n const [jumping, setJumping] = useState(false)\r\n // Hover full-content panel: index of the hovered item + fixed position.\r\n // The tip only triggers after the expand animation settles — in the\r\n // collapsed state the item rect is only 36px wide, so a tip positioned\r\n // there would be wrong once the rail expands.\r\n const [tip, setTip] = useState<{ index: number; x: number; y: number } | null>(null)\r\n const navRef = useRef<HTMLDivElement | null>(null) // True only after the expand animation has fully settled; the width\r\n // transition takes ~250ms after `show` flips, and item rects are only\r\n // stable once it finishes. Tip positioning must wait for this.\r\n const expandedRef = useRef(false)\r\n // Last observed pointer position over the rail. A mouseenter that arrives\r\n // before the width transition cannot show the tip (rects unstable), yet the\r\n // enter event is already gone by the time the animation ends — the settle\r\n // handler re-hit-tests the cursor against this position to show the tip for\r\n // the node still under the pointer.\r\n const lastPointerRef = useRef<{ x: number; y: number } | null>(null)\r\n // Aborts an in-flight jump (component unmount or a newer click superseding\r\n // an older one), so the loadOlder/DOM-poll loops stop promptly.\r\n const jumpAbortRef = useRef<AbortController | null>(null)\r\n useEffect(() => () => jumpAbortRef.current?.abort(), [])\r\n\r\n // Track the cursor over the rail for the expand-settled tip re-hit-test\r\n // (the `show` effect's settle handler reads lastPointerRef). Native\r\n // listener: plain DOM events keep the callback typed without React\r\n // synthetic-event generics in the hand-written createElement props.\r\n useEffect(() => {\r\n const el = navRef.current\r\n if (el === null) return\r\n const onMove = (e: PointerEvent) => { lastPointerRef.current = { x: e.clientX, y: e.clientY } }\r\n el.addEventListener('pointermove', onMove, { passive: true })\r\n return () => el.removeEventListener('pointermove', onMove)\r\n }, [])\r\n\r\n /** Position the tip against the item's CURRENT (post-expand) rect. */\r\n const positionTip = (index: number) => {\r\n if (index < 0) return\r\n const el = navRef.current?.querySelector<HTMLElement>(`[data-crl-index=\"${index}\"]`)\r\n if (el === null || el === undefined) return\r\n const rect = el.getBoundingClientRect()\r\n setTip({ index, x: rect.left - 12, y: rect.top })\r\n }\r\n\r\n /** Handle item hover: the collapsed state only expands the rail; the tip\r\n * only triggers once the expand animation has settled (item rects are then\r\n * stable, so the tip is positioned correctly). */\r\n const handleItemEnter = (index: number) => {\r\n if (!expandedRef.current) {\r\n setShow(true)\r\n return\r\n }\r\n positionTip(index)\r\n }\r\n\r\n const handleItemLeave = (index: number) => {\r\n setTip((prev) => (prev?.index === index ? null : prev))\r\n }\r\n\r\n // Track the settled expand state: when `show` flips true, wait for the width\r\n // transition to end before allowing tip triggers; when it flips false, the\r\n // rail is collapsing — treat as unexpanded immediately.\r\n useEffect(() => {\r\n if (!show) {\r\n expandedRef.current = false\r\n return\r\n }\r\n const el = navRef.current\r\n if (el === null) return\r\n let settled = false\r\n const settle = () => {\r\n if (settled) return\r\n settled = true\r\n expandedRef.current = true\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n // Re-hit-test the cursor: a mouseenter that fired before the width\r\n // transition could not show the tip (rects were still unstable), and\r\n // the enter event is long gone once the animation ends. If the pointer\r\n // still sits on a rail item, position that item's tip now.\r\n const p = lastPointerRef.current\r\n if (p !== null) {\r\n const hit = document.elementFromPoint(p.x, p.y)\r\n const item = hit === null ? null : hit.closest('[data-crl-index]')\r\n const idx = item === null ? -1 : Number(item.getAttribute('data-crl-index'))\r\n if (Number.isInteger(idx) && idx >= 0) positionTip(idx)\r\n }\r\n }\r\n const onTransitionEnd = (e: TransitionEvent) => {\r\n if (e.propertyName !== 'width') return\r\n settle()\r\n }\r\n el.addEventListener('transitionend', onTransitionEnd)\r\n // Fallback for `prefers-reduced-motion: reduce` (the stylesheet disables\r\n // the width transition, so `transitionend` never fires) and for any lost\r\n // event: settle after the transition duration regardless.\r\n const timer = setTimeout(settle, 300)\r\n return () => {\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n }\r\n }, [show])\r\n\r\n // Session-scoped attachment reader for tip thumbnails (stable identity so\r\n // TipThumb effects do not re-run every render; resolveThumb caches anyway).\r\n const readThumb = useMemo<ReadAttachmentFn | undefined>(() => {\r\n if (sessionId === undefined) return undefined\r\n const s = (sessionsService.binding(sessionId) as unknown as\r\n { session?: { readAttachment?: ReadAttachmentFn } } | undefined)?.session\r\n return s?.readAttachment === undefined\r\n ? undefined\r\n : (id: string) => s.readAttachment!(id)\r\n }, [sessionId, sessionsService])\r\n\r\n // Fill-to-input (plus button): put the message's full text into the draft\r\n // and re-attach its stored images through the official draft-image path\r\n // (readAttachment → File → createDraftImages → addImages). Degrades to\r\n // text-only when the conversation service or storage read is unavailable.\r\n const fillToInput = useCallback((messageId: string) => {\r\n const m = messages.find((candidate) => favoriteIdOfMessage(candidate) === messageId)\r\n if (m === undefined || inputActions === undefined || sessionId === undefined) return\r\n const text = fullTextOf(m, nodeSnapshot) || m.text\r\n if (inputActions.setDraft !== undefined) inputActions.setDraft(text)\r\n const specs = tipImagesOf(m, nodeSnapshot).filter((spec): spec is ImageSpec & { kind: 'ref' } => spec.kind === 'ref')\r\n if (specs.length === 0 || conversation === undefined || readThumb === undefined) return\r\n void (async () => {\r\n const files: File[] = []\r\n for (const spec of specs) {\r\n try {\r\n const result = await readThumb(spec.attachmentId) as unknown as { ok?: boolean; value?: { attachment?: { mediaType?: string }; data?: Uint8Array } }\r\n if (result?.ok !== true || result.value?.data === undefined) continue\r\n const mediaType = result.value.attachment?.mediaType ?? spec.mediaType\r\n files.push(new File([result.value.data as BlobPart], `attachment-${spec.attachmentId}${mediaType.includes('jpeg') ? '.jpg' : '.png'}`, { type: mediaType }))\r\n } catch {\r\n // One failed attachment must not block the others.\r\n }\r\n }\r\n if (files.length === 0) return\r\n const drafts = conversation.createDraftImages(files)\r\n const ids = drafts.map((draft) => draft.id).filter((id): id is string => id !== undefined)\r\n if (ids.length > 0) {\r\n try { inputActions.addImages?.(ids) } catch { /* draft-image registry may reject; text fill already landed */ }\r\n }\r\n })()\r\n }, [conversation, inputActions, messages, nodeSnapshot, readThumb, sessionId])\r\n\r\n // Bridge the DOM-injected buttons (module-level actionCtx) to this\r\n // component's live closures + session id; refresh star states on every\r\n // favorites change and on session switch.\r\n useEffect(() => {\r\n actionCtx.sessionId = sessionId\r\n actionCtx.onToggleFavorite = (messageId) => {\r\n if (sessionId !== undefined) toggleFavorite(sessionId, messageId)\r\n }\r\n actionCtx.onFill = fillToInput\r\n refreshAllFavoriteButtons(t)\r\n return () => {\r\n // Only clear what this render set: a newer session may already own the ctx.\r\n if (actionCtx.sessionId === sessionId) actionCtx.sessionId = undefined\r\n }\r\n }, [sessionId, fillToInput, t])\r\n\r\n // Keep the injected DOM buttons in sync with external favorite toggles\r\n // (rail star / another session's store write).\r\n useEffect(() => {\r\n refreshAllFavoriteButtons(t)\r\n }, [favorites, t])\r\n\r\n // Inject star + plus buttons into every user message row; observe DOM\r\n // changes so history paging and re-renders keep them present.\r\n useEffect(() => {\r\n if (typeof document === 'undefined' || document.body === null) return\r\n return startActionInjector(t)\r\n }, [t])\r\n\r\n // Background full-history load: follow the runtime's authoritative hasMore\r\n // flag, but STOP as soon as the projection delivers.\r\n useEffect(() => {\r\n if (session === undefined) return\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n let cancelled = false\r\n const run = async () => {\r\n let guard = 0\r\n while (!cancelled && guard++ < 120) {\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n const snap = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean }\r\n if (snap?.hasMore !== true) return\r\n if (snap.loadingOlder === true) { await delay(50); continue }\r\n await (session as unknown as { loadOlder(): Promise<unknown> }).loadOlder()\r\n }\r\n }\r\n run().catch(() => {})\r\n return () => { cancelled = true }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, session === undefined ? 'none' : 'ready', Array.isArray(projected?.messages) && projected.messages.length > 0 ? 'have' : 'none'])\r\n\r\n // Track the reading position (active item): nearest row to the 40% viewport line.\r\n useEffect(() => {\r\n if (effectiveMessages.length === 0) return\r\n const indexByKey = new Map<string, number>()\r\n for (let i = 0; i < effectiveMessages.length; i++) {\r\n const key = anchorKeyOf(effectiveMessages[i])\r\n if (key !== undefined) indexByKey.set(key, i)\r\n }\r\n const updateActive = () => {\r\n const sp = document.querySelector('[data-conversation-scroll]')\r\n if (sp === null) return\r\n const rect = sp.getBoundingClientRect()\r\n if (rect.height === 0) return\r\n const line = rect.top + rect.height * 0.4\r\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"]')\r\n let best = -1\r\n let bestDist = Infinity\r\n for (const row of rows) {\r\n const key = row.getAttribute('data-chat-anchor-key')\r\n if (key === null) continue\r\n const idx = indexByKey.get(key) ?? -1\r\n if (idx === -1) continue\r\n const r = row.getBoundingClientRect()\r\n const dist = Math.abs(r.top + r.height / 2 - line)\r\n if (dist < bestDist) { bestDist = dist; best = idx }\r\n }\r\n setActiveIndex(best)\r\n }\r\n updateActive()\r\n const el = document.querySelector('[data-conversation-scroll]')\r\n let scrollTimer: ReturnType<typeof setTimeout> | null = null\r\n const onScroll = () => {\r\n if (scrollTimer !== null) return\r\n scrollTimer = setTimeout(() => { scrollTimer = null; updateActive() }, 60)\r\n }\r\n el?.addEventListener('scroll', onScroll, { passive: true })\r\n const timer = window.setInterval(updateActive, 2000)\r\n return () => {\r\n el?.removeEventListener('scroll', onScroll)\r\n window.clearInterval(timer)\r\n if (scrollTimer !== null) clearTimeout(scrollTimer)\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, effectiveMessages.length])\r\n\r\n // Scroll the rail so the active item stays centered in its visible area:\r\n // follows the conversation's reading position (and re-centers after a\r\n // session switch, where activeIndex is recomputed for the new session).\r\n useEffect(() => {\r\n if (activeIndex < 0) return\r\n const el = navRef.current\r\n if (el === null) return\r\n const item = el.querySelector<HTMLElement>(`[data-crl-index=\"${activeIndex}\"]`)\r\n if (item === null) return\r\n const target = item.offsetTop - el.clientHeight / 2 + item.clientHeight / 2\r\n el.scrollTo({ top: Math.max(0, target), behavior: 'smooth' })\r\n }, [activeIndex])\r\n\r\n // The rail hides on tiny sessions (fewer than 2 user messages) — but the\r\n // count is the UNFILTERED list: the favorites-only filter may legitimately\r\n // narrow to a single row, and hiding the rail then would strand the user\r\n // with no way to switch the filter back off.\r\n if (sessionId === undefined || messages.length < 2) return null\r\n\r\n // Fav-only filter remaps the tip index into the FULL list (tip content and\r\n // hover data always come from the unfiltered messages array).\r\n const tipIndex = tip === null\r\n ? -1\r\n : favOnly\r\n ? messages.findIndex((m) => effectiveMessages[tip.index] !== undefined\r\n && favoriteIdOfMessage(m) === favoriteIdOfMessage(effectiveMessages[tip.index]))\r\n : tip.index\r\n\r\n const items = effectiveMessages.map((m, i) => {\r\n const key = anchorKeyOf(m)\r\n const starred = favoriteIdOfMessage(m) !== '' && isFavorite(favorites, sessionId, favoriteIdOfMessage(m))\r\n return createElement('button', {\r\n type: 'button',\r\n key: m.seq,\r\n 'data-crl-index': String(i),\r\n className: S.item + (activeIndex === i ? ` ${S.itemActive}` : '') + (starred ? ` ${S.favItem}` : ''),\r\n 'aria-label': `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,\r\n 'aria-current': activeIndex === i ? 'location' : undefined,\r\n disabled: jumping,\r\n onClick: () => {\r\n if (key === undefined || jumping) return\r\n jumpAbortRef.current?.abort()\r\n const controller = new AbortController()\r\n jumpAbortRef.current = controller\r\n setJumping(true)\r\n void jumpToMessage(sessionsService as never, sessionId as string, key, undefined, controller.signal)\r\n .finally(() => setJumping(false))\r\n },\r\n onMouseEnter: () => handleItemEnter(i),\r\n onMouseLeave: () => handleItemLeave(i),\r\n children: [\r\n createElement('span', { className: S.num }, `#${i + 1}`),\r\n starred\r\n ? createElement('span', { className: S.favStar, role: 'img', 'aria-label': t.fav, 'aria-hidden': undefined },\r\n createElement('svg', {\r\n viewBox: '0 0 24 24',\r\n width: 13,\r\n height: 13,\r\n fill: 'currentColor',\r\n stroke: 'none',\r\n 'aria-hidden': true,\r\n },\r\n createElement('path', { d: STAR_PATH, 'aria-hidden': true })))\r\n : null,\r\n createElement('span', { className: S.title }, m.text === '' ? t.noText : m.text),\r\n m.hasImage\r\n ? createElement('span', { className: S.img, role: 'img', 'aria-label': t.hasImage },\r\n createElement('svg', {\r\n viewBox: '0 0 16 16',\r\n width: 12,\r\n height: 12,\r\n fill: 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 1.4,\r\n strokeLinecap: 'round',\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n },\r\n createElement('rect', { x: 1.7, y: 2.7, width: 12.6, height: 10.6, rx: 2.2 }),\r\n createElement('circle', { cx: 5.6, cy: 6.4, r: 1.15, fill: 'currentColor', stroke: 'none' }),\r\n createElement('path', { d: 'M2.5 11.6l3.6-3.1 2.6 2.2 2.1-1.9 3.3 2.8' })))\r\n : null,\r\n createElement('span', { className: S.time }, relativeTime(m.time, t)),\r\n createElement('span', { className: S.ind, 'aria-hidden': true },\r\n createElement('span', { className: S.line })),\r\n ],\r\n })\r\n })\r\n\r\n return createPortal(\r\n // Fragment: the nav (transformed) and the tip must be siblings — a fixed\r\n // child inside a transformed ancestor is positioned relative to that\r\n // ancestor, breaking the viewport coordinates the tip uses.\r\n [createElement('div', {\r\n ref: navRef,\r\n className: S.nav + (show ? ` ${S.navShow}` : ''),\r\n role: 'navigation',\r\n 'aria-label': t.railLabel,\r\n onMouseEnter: () => setShow(true),\r\n onMouseLeave: () => setShow(false),\r\n children: [\r\n // Favorites-only filter: a star pill pinned at the top of the rail\r\n // (sticky; it never scrolls away with the rows). On state fills the\r\n // star (yellow) and narrows the rail to favorited messages; the label\r\n // (\"bookmarks only\") only shows once the rail expands — the collapsed\r\n // 36px capsule keeps a clean round icon. Hidden entirely when this\r\n // session has no favorites (nothing to filter to).\r\n showFavToggle\r\n ? createElement('button', {\r\n type: 'button',\r\n key: 'favToggle',\r\n className: S.favToggle + (favOnly ? ' crl_on' : ''),\r\n 'aria-pressed': favOnly,\r\n 'aria-label': t.favOnly,\r\n title: t.favOnly,\r\n onMouseEnter: (e: MouseEvent) => e.stopPropagation(),\r\n onClick: () => setFavOnly((v) => !v),\r\n children: [\r\n createElement('svg', {\r\n viewBox: '0 0 24 24',\r\n width: 14,\r\n height: 14,\r\n fill: favOnly ? 'currentColor' : 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 2,\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n }, createElement('path', { d: STAR_PATH })),\r\n createElement('span', { className: S.favToggleLabel }, t.favOnly),\r\n ],\r\n })\r\n : null,\r\n jumping ? createElement('div', { className: S.loading, key: 'loading' },\r\n createElement('span', { className: S.loadingLabel }, t.loading)) : null,\r\n ...items,\r\n ],\r\n }),\r\n // Full-content panel: anchored to the left of the hovered item. Images\r\n // stack above the text; a message whose image cannot be resolved (no\r\n // projection refs and unloaded node) shows a quiet \"has image\" badge.\r\n tip !== null && tipIndex >= 0 && tipIndex < messages.length\r\n ? createElement('div', {\r\n className: S.tip,\r\n style: { left: `${tip.x}px`, top: `${tip.y}px`, transform: 'translateX(-100%)' },\r\n }, (() => {\r\n const m = messages[tipIndex]\r\n const specs = tipImagesOf(m, nodeSnapshot)\r\n const children: ReactNode[] = []\r\n if (specs.length > 0 && readThumb !== undefined) {\r\n // Only the first image renders; the rest collapse into a \"+N\"\r\n // badge so a many-image message cannot push the tip past its\r\n // viewport cap (the stacked thumbnails did).\r\n children.push(createElement('div', { className: S.tipImgs, key: 'imgs' },\r\n createElement('div', { className: S.tipImgWrap, key: 'img' },\r\n createElement(TipThumb, { spec: specs[0], sessionId: sessionId as string, read: readThumb }),\r\n specs.length > 1\r\n ? createElement('span', { className: S.tipImgCount, 'aria-hidden': true }, `+${specs.length - 1}`)\r\n : null)))\r\n } else if (m.hasImage) {\r\n children.push(createElement('span', { className: S.tipBadge, key: 'badge' }, t.hasImage))\r\n }\r\n children.push(createElement('span', { key: 'text' }, fullTextOf(m, nodeSnapshot) || t.noText))\r\n return children\r\n })())\r\n : null],\r\n document.body,\r\n )\r\n}\r\n\r\nfunction apply(ctx: ClientContext): void {\r\n 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 // 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}\r\n\r\nexport { apply, TimelineRail, imageSpecsOfContent, normalize }\r\n"],"mappings":"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"}
package/lib/index.mjs CHANGED
@@ -1,7 +1,9 @@
1
+ import { homedir } from "node:os";
2
+ import { dirname, join } from "node:path";
3
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
4
  //#region src/index.ts
2
5
  /**
3
6
  * @max-null/dsh-chat-rail — host half.
4
- *
5
7
  * Registers the `chatRail` session projection unit: a complete, durable
6
8
  * enumeration of the session's USER-sent messages (seq / time / preview /
7
9
  * durable message id). The client rail only needs user turns; assistant
@@ -29,9 +31,31 @@ function hasImageBlock(content) {
29
31
  if (!Array.isArray(content)) return false;
30
32
  return content.some((block) => block !== null && typeof block === "object" && block.type === "image");
31
33
  }
34
+ /** Collect stored-image references from a ContentBlock list (reference form only:
35
+ * inline base64 data stays out of the payload — it can be megabytes). */
36
+ function imageRefsOf(content) {
37
+ if (!Array.isArray(content)) return [];
38
+ const refs = [];
39
+ for (const block of content) {
40
+ if (block === null || typeof block !== "object") continue;
41
+ const b = block;
42
+ if (b.type !== "image") continue;
43
+ const a = b.attachment;
44
+ if (a === null || typeof a !== "object") continue;
45
+ const ref = a;
46
+ if (typeof ref.attachmentId !== "string" || ref.attachmentId === "") continue;
47
+ refs.push({
48
+ attachmentId: ref.attachmentId,
49
+ mediaType: typeof ref.mediaType === "string" ? ref.mediaType : "image/png",
50
+ width: typeof ref.width === "number" ? ref.width : 0,
51
+ height: typeof ref.height === "number" ? ref.height : 0
52
+ });
53
+ }
54
+ return refs;
55
+ }
32
56
  const messageIndexProjectionDefinition = {
33
57
  key: PROJECTION_KEY,
34
- schema: { parse: (val) => val },
58
+ stateSchema: { parse: (val) => val },
35
59
  init: () => ({ messages: [] }),
36
60
  apply: (state, event) => {
37
61
  if (event.type === "user/message") {
@@ -46,23 +70,92 @@ const messageIndexProjectionDefinition = {
46
70
  time: event.time,
47
71
  text,
48
72
  hasImage,
73
+ images: imageRefsOf(data.content),
49
74
  id
50
75
  }] };
51
76
  }
52
77
  return state;
53
78
  },
54
- view: (state) => state,
55
- stateVersion: 5
79
+ wire: {
80
+ viewSchema: { parse: (val) => val },
81
+ view: (state) => state
82
+ },
83
+ stateVersion: 6
56
84
  };
57
85
  const Config = { "~standard": {
58
86
  version: 1,
59
87
  vendor: "chat-rail",
60
88
  validate: (value) => ({ value: value ?? {} })
61
89
  } };
90
+ const FAVORITES_PATH = join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "chat-rail-favorites.json");
91
+ const FAVORITES_ROUTE_PREFIX = "/chat-rail/api/favorites";
92
+ function readFavoritesFile() {
93
+ try {
94
+ const parsed = JSON.parse(readFileSync(FAVORITES_PATH, "utf8"));
95
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
96
+ } catch {
97
+ return {};
98
+ }
99
+ }
100
+ function writeFavoritesFile(map) {
101
+ try {
102
+ mkdirSync(dirname(FAVORITES_PATH), { recursive: true });
103
+ writeFileSync(FAVORITES_PATH, JSON.stringify(map), "utf8");
104
+ } catch {}
105
+ }
106
+ function sendJson(res, status, body) {
107
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
108
+ res.end(JSON.stringify(body));
109
+ }
110
+ const favoritesRouteDefinition = {
111
+ kind: "exact",
112
+ path: FAVORITES_ROUTE_PREFIX,
113
+ handler: async (req, res) => {
114
+ if (req.method === "GET") {
115
+ sendJson(res, 200, {
116
+ ok: true,
117
+ value: readFavoritesFile()
118
+ });
119
+ return;
120
+ }
121
+ if (req.method === "PUT") {
122
+ let raw = "";
123
+ req.on("data", (chunk) => {
124
+ raw += chunk;
125
+ });
126
+ await new Promise((resolve) => req.on("end", () => resolve()));
127
+ try {
128
+ const map = JSON.parse(raw)?.favorites;
129
+ if (map === null || typeof map !== "object" || Array.isArray(map)) {
130
+ sendJson(res, 400, {
131
+ ok: false,
132
+ error: "bad-request"
133
+ });
134
+ return;
135
+ }
136
+ writeFavoritesFile(map);
137
+ sendJson(res, 200, { ok: true });
138
+ } catch {
139
+ sendJson(res, 400, {
140
+ ok: false,
141
+ error: "bad-request"
142
+ });
143
+ }
144
+ return;
145
+ }
146
+ sendJson(res, 405, {
147
+ ok: false,
148
+ error: "method-not-allowed"
149
+ });
150
+ }
151
+ };
62
152
  function apply(ctx) {
63
- ctx.inject(["sessionProjections"], (projectionCtx) => {
153
+ ctx.inject(["sessionProjections"], ((projectionCtx) => {
64
154
  projectionCtx.sessionProjections.register(messageIndexProjectionDefinition);
65
- });
155
+ }));
156
+ ctx.inject(["webServer"], ((wsCtx) => {
157
+ wsCtx.webServer.register(favoritesRouteDefinition);
158
+ }));
66
159
  }
67
160
  //#endregion
68
161
  export { Config, apply, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-chat-rail",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "画卷式消息导航栏:右侧竖条 hover 展开画卷,scroll-spy 高亮居中跟随,编号+时间+完整预览,与 better-sidebar 双面板动画同步避让",
5
5
  "type": "module",
6
6
  "main": "lib/index.mjs",
@@ -58,33 +58,27 @@
58
58
  ],
59
59
  "peerDependencies": {
60
60
  "@deepseek-ai/cordis": "^4.0.1",
61
- "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.6",
62
- "dsh-better-sidebar": ">=0.12.0",
61
+ "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.1",
63
62
  "react": "^18.2.0",
64
63
  "react-dom": "^18.2.0"
65
64
  },
66
- "peerDependenciesMeta": {
67
- "dsh-better-sidebar": {
68
- "optional": true
69
- }
70
- },
71
65
  "devDependencies": {
72
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
73
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
74
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
75
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
76
- "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.6",
66
+ "@deepseek-ai/cordis": "^4.0.1",
67
+ "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
68
+ "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.1",
69
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.1",
70
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
71
+ "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.1",
77
72
  "@types/node": "^22.0.0",
78
73
  "@types/react": "~18.3.1",
79
74
  "@types/react-dom": "^18.3.7",
80
75
  "cordis": "^4.0.0-rc.7",
81
- "dsh-better-sidebar": "^0.13.0",
76
+ "playwright-core": "1.57.0",
82
77
  "react": "^18.2.0",
83
78
  "react-dom": "^18.2.0",
84
79
  "tsdown": "^0.22.2",
85
80
  "tsx": "^4.0.0",
86
81
  "typescript": "^5.6.0",
87
- "unrun": "^0.3.1",
88
- "@deepseek-ai/cordis": "^4.0.1"
82
+ "unrun": "^0.3.1"
89
83
  }
90
84
  }
Binary file
Binary file