@max-null/dsh-chat-rail 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +34 -6
- package/lib/client.js.map +1 -1
- package/package.json +89 -88
package/lib/client.js
CHANGED
|
@@ -67,7 +67,7 @@ window.__ModuleLoader__.load({
|
|
|
67
67
|
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
68
68
|
}
|
|
69
69
|
const css = [
|
|
70
|
-
".crl_nav{user-select:none;z-index:100;position:fixed;right:calc(var(--dsh-sidebar-width,0px) +
|
|
70
|
+
".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}",
|
|
71
71
|
"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}",
|
|
72
72
|
".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)}",
|
|
73
73
|
"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)}",
|
|
@@ -189,16 +189,27 @@ window.__ModuleLoader__.load({
|
|
|
189
189
|
* loadOlder pages 50 messages at a time (DSH PAGE_MESSAGES), so a jump far back
|
|
190
190
|
* in a long session needs many pages. Report page count through onProgress so
|
|
191
191
|
* the UI can show a loading state instead of appearing frozen.
|
|
192
|
+
*
|
|
193
|
+
* The paged-in data lands in the session snapshot synchronously with
|
|
194
|
+
* loadOlder's resolution, but the chat rows render through React — the DOM row
|
|
195
|
+
* for the target key appears a beat later. We therefore poll for the row (with
|
|
196
|
+
* a timeout) instead of querying once and giving up; otherwise a multi-page
|
|
197
|
+
* jump would "finish loading" without scrolling and need a second click.
|
|
192
198
|
*/
|
|
193
|
-
async function jumpToMessage(sessionsService, sessionId, key, onProgress) {
|
|
199
|
+
async function jumpToMessage(sessionsService, sessionId, key, onProgress, signal) {
|
|
194
200
|
const session = sessionsService.binding(sessionId)?.session;
|
|
195
201
|
if (session === void 0) return false;
|
|
196
202
|
let pages = 0;
|
|
197
203
|
let guard = 0;
|
|
204
|
+
let loaded = false;
|
|
198
205
|
while (guard++ < 120) {
|
|
206
|
+
if (signal?.aborted) return false;
|
|
199
207
|
const snapshot = session.getSnapshot();
|
|
200
|
-
if (snapshot?.chat?.nodes?.get(key) !== void 0)
|
|
201
|
-
|
|
208
|
+
if (snapshot?.chat?.nodes?.get(key) !== void 0) {
|
|
209
|
+
loaded = true;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
if (snapshot?.hasMore !== true) break;
|
|
202
213
|
if (snapshot.loadingOlder === true) {
|
|
203
214
|
await delay(50);
|
|
204
215
|
continue;
|
|
@@ -207,8 +218,20 @@ window.__ModuleLoader__.load({
|
|
|
207
218
|
pages++;
|
|
208
219
|
onProgress?.(pages);
|
|
209
220
|
}
|
|
221
|
+
if (!loaded) {
|
|
222
|
+
console.warn(`[chat-rail] jumpToMessage: node "${key}" not loaded after ${pages} page(s)`);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
210
225
|
const scrollport = typeof document !== "undefined" ? document.querySelector("[data-conversation-scroll]") : null;
|
|
211
|
-
|
|
226
|
+
if (scrollport === null) return false;
|
|
227
|
+
let row = null;
|
|
228
|
+
let waited = 0;
|
|
229
|
+
while (waited++ < 100) {
|
|
230
|
+
if (signal?.aborted) return false;
|
|
231
|
+
row = scrollport.querySelector(`[data-chat-anchor-key="${CSS.escape(key)}"]`);
|
|
232
|
+
if (row !== null) break;
|
|
233
|
+
await delay(50);
|
|
234
|
+
}
|
|
212
235
|
if (row === null) return false;
|
|
213
236
|
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
214
237
|
row.scrollIntoView({
|
|
@@ -237,6 +260,8 @@ window.__ModuleLoader__.load({
|
|
|
237
260
|
const [tip, setTip] = (0, react.useState)(null);
|
|
238
261
|
const navRef = (0, react.useRef)(null);
|
|
239
262
|
const expandedRef = (0, react.useRef)(false);
|
|
263
|
+
const jumpAbortRef = (0, react.useRef)(null);
|
|
264
|
+
(0, react.useEffect)(() => () => jumpAbortRef.current?.abort(), []);
|
|
240
265
|
/** Position the tip against the item's CURRENT (post-expand) rect. */
|
|
241
266
|
const positionTip = (index) => {
|
|
242
267
|
if (index < 0) return;
|
|
@@ -390,8 +415,11 @@ window.__ModuleLoader__.load({
|
|
|
390
415
|
disabled: jumping,
|
|
391
416
|
onClick: () => {
|
|
392
417
|
if (key === void 0 || jumping) return;
|
|
418
|
+
jumpAbortRef.current?.abort();
|
|
419
|
+
const controller = new AbortController();
|
|
420
|
+
jumpAbortRef.current = controller;
|
|
393
421
|
setJumping(true);
|
|
394
|
-
jumpToMessage(sessionsService, sessionId, key).finally(() => setJumping(false));
|
|
422
|
+
jumpToMessage(sessionsService, sessionId, key, void 0, controller.signal).finally(() => setJumping(false));
|
|
395
423
|
},
|
|
396
424
|
onMouseEnter: () => handleItemEnter(i),
|
|
397
425
|
onMouseLeave: () => handleItemLeave(i),
|
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":["/**\n * @max-null/dsh-chat-rail — web client half.\n *\n * Right-edge conversation navigation rail: one indicator per user message,\n * scroll-spy highlight of the reading position, hover preview, click to jump.\n *\n * Two deliberate fixes over the dsh-chat-timeline reference:\n *\n * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout\n * push through the `--dsh-sidebar-width` CSS variable on `:root` (the\n * `#root` margin-right transition reads the same variable). The rail's\n * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same\n * transition timing, so rail and scrollport move together while the panel\n * expands — no \"panel first, scrollbar later\" lag.\n *\n * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)\n * lines — nearly invisible on a light background. Here the rail keeps a\n * constant translucent capsule, thicker/darker indicator lines, and an\n * always-visible collapsed state (no \"reveal on pointer proximity\").\n *\n * Data: host `chatRail` projection first, loaded chat nodes fallback, then a\n * background loadOlder loop (stopped as soon as the projection delivers).\n * Mounted in conversation.input.dock, portal-rendered to body.\n */\n\nimport { createElement, useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry).\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n\nexport const inject = ['slots', 'sessions']\n\n// ---- i18n (DSH locale-aware, zh/en) ----\ntype LocaleId = 'zh' | 'en'\nconst STRINGS: Record<LocaleId, Record<string, string>> = {\n zh: {\n railLabel: '消息导航',\n roleUser: '用户',\n noText: '(无文本内容)',\n ariaJump: '跳转到消息',\n loading: '加载中…',\n timeJustNow: '刚刚',\n timeMinutes: '{n}分钟前',\n timeHours: '{n}小时前',\n timeDays: '{n}天前',\n },\n en: {\n railLabel: 'Message rail',\n roleUser: 'User',\n noText: '(no text)',\n ariaJump: 'Jump to message',\n loading: 'Loading…',\n timeJustNow: 'just now',\n timeMinutes: '{n}m ago',\n timeHours: '{n}h ago',\n timeDays: '{n}d ago',\n },\n}\n\n/** Compact relative time for a message timestamp (zh/en via template). */\nfunction relativeTime(ts: number, s: Record<string, string>): string {\n const diff = Date.now() - ts\n if (diff < 60_000) return s.timeJustNow\n if (diff < 3_600_000) return s.timeMinutes.replace('{n}', String(Math.floor(diff / 60_000)))\n if (diff < 86_400_000) return s.timeHours.replace('{n}', String(Math.floor(diff / 3_600_000)))\n if (diff < 7 * 86_400_000) return s.timeDays.replace('{n}', String(Math.floor(diff / 86_400_000)))\n const d = new Date(ts)\n return `${d.getMonth() + 1}/${d.getDate()}`\n}\n\n// ---- styles ----\n// 画卷式导航:单个容器,折叠态 36px 竖条;hover 时宽度从右往左展开到 280px\n// (打开画卷效果)。容器垂直方向在「去掉底栏后的可用区域」内居中:\n// top = (100vh - 底栏高)/2 + translateY(-50%),底栏收起时正中、展开时上移避让。\n// right 跟随 --dsh-sidebar-width、top 跟随 --dsh-sidebar-height,与 better-sidebar\n// 面板共享同一 CSS 变量 + transition,动画同步。\nconst css = [\n '.crl_nav{user-select:none;z-index:100;position:fixed;right:calc(var(--dsh-sidebar-width,0px) + 12px);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}',\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}',\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)}',\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)}',\n '.crl_nav::-webkit-scrollbar{width:4px}',\n '.crl_nav::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);border-radius:4px}',\n // Jump-in-progress indicator: sticky row pinned at the rail top. The spinner\n // icon is always visible; the \"Loading…\" label only appears once expanded\n // (in the collapsed 36px rail the text would overflow the capsule).\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}',\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}',\n '.crl_loading .crl_loadingLabel{display:none}',\n '.crl_show .crl_loading .crl_loadingLabel{display:inline}',\n '@keyframes crl-spin{to{transform:rotate(360deg)}}',\n // Item row: always flex-end (the indicator rides the right edge through the\n // width animation, never jumping). The collapsed padding (0 9px) centers the\n // 18px indicator in the 36px capsule; the expanded padding (0 14px) clears\n // the text. Padding transitions with the same timing as the width, so the\n // indicator glides smoothly instead of snapping when text hides/shows.\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}',\n '.crl_show .crl_item{padding:0 14px}',\n '.crl_item:hover{color:rgba(0,0,0,.95)}',\n '.crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\n 'body[data-ds-dark-theme] .crl_item,[data-theme=\\'dark\\'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}',\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)}',\n // Title / ordinal / time: display:none in collapsed state (so they take no\n // flex space and the indicator line stays centered), revealed on expand.\n // The ordinal has a fixed width (right-aligned) so message text starts at\n // the same x across items; time sits AFTER the title (no fixed width, so it\n // never shrinks the text area).\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}',\n '.crl_show .crl_title{display:block;animation:crl-fade .18s ease}',\n '.crl_item.crl_active .crl_title{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:500}',\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}',\n '.crl_show .crl_num{display:block;animation:crl-fade .18s ease}',\n 'body[data-ds-dark-theme] .crl_num,[data-theme=\\'dark\\'] .crl_num,.dark .crl_num{color:rgba(255,255,255,.35)}',\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}',\n '.crl_show .crl_time{display:block;animation:crl-fade .18s ease}',\n 'body[data-ds-dark-theme] .crl_time,[data-theme=\\'dark\\'] .crl_time,.dark .crl_time{color:rgba(255,255,255,.28)}',\n '@keyframes crl-fade{from{opacity:0}to{opacity:1}}',\n // Indicator line: 10×3px, clearly visible in both themes.\n '.crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}',\n '.crl_show .crl_ind{margin-left:8px}',\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}',\n '.crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}',\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)}',\n 'body[data-ds-dark-theme] .crl_line,[data-theme=\\'dark\\'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}',\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)}',\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)}',\n // Full-content hover panel: floats to the LEFT of the expanded rail list.\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}',\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)))}',\n '@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}}',\n].join('')\n\nconst STYLE_ID = '@max-null/dsh-chat-rail/styles.module.css'\nif (typeof document !== 'undefined' && document.querySelector(`style[data-plugin-css=\"${STYLE_ID}\"]`) === null) {\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-chat-rail'\n tag.dataset.pluginCss = STYLE_ID\n tag.textContent = css\n document.head.appendChild(tag)\n}\n\nconst S = {\n nav: 'crl_nav',\n navShow: 'crl_show',\n item: 'crl_item',\n itemActive: 'crl_active',\n title: 'crl_title',\n num: 'crl_num',\n time: 'crl_time',\n ind: 'crl_ind',\n line: 'crl_line',\n loading: 'crl_loading',\n loadingLabel: 'crl_loadingLabel',\n tip: 'crl_tip',\n}\n\n// ---- data helpers ----\nconst NOOP_STORE = { getSnapshot: () => undefined, subscribe: () => () => {} }\n\ninterface RailMessage {\n seq: number\n time: number\n text: string\n key?: string\n id?: string\n}\n\n/** Extract preview text from a user message's ContentBlock list. */\nfunction userTextOf(content: unknown): string {\n if (!Array.isArray(content)) return ''\n let out = ''\n for (const block of content) {\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\n && typeof (block as { text?: unknown }).text === 'string') {\n out += (block as { text: string }).text\n }\n }\n return out.trim().slice(0, 80)\n}\n\n/** Normalize one projection entry to a rail message. */\nfunction normalize(m: unknown): RailMessage | null {\n if (m === null || typeof m !== 'object') return null\n const o = m as Record<string, unknown>\n if (typeof o.seq !== 'number') return null\n // Host projection emits `text`; keep `preview` as a defensive fallback in\n // case an older projection payload is still cached in the browser.\n const text = typeof o.text === 'string' ? o.text : typeof o.preview === 'string' ? o.preview : ''\n return {\n seq: o.seq,\n time: typeof o.time === 'number' ? o.time : 0,\n text,\n ...(typeof o.key === 'string' ? { key: o.key } : {}),\n ...(typeof o.id === 'string' ? { id: o.id } : {}),\n }\n}\n\n/** Fallback collector: enumerate user messages from the loaded chat nodes. */\nfunction collectFromNodes(snapshot: unknown): RailMessage[] {\n const out: RailMessage[] = []\n if (snapshot === undefined || (snapshot as { chat?: unknown }).chat === undefined) return out\n const chat = (snapshot as { chat: { nodes?: Map<unknown, unknown> } }).chat\n if (!chat.nodes) return out\n for (const node of chat.nodes.values()) {\n if (node === null || typeof node !== 'object') continue\n const n = node as { kind?: unknown; key?: unknown; anchorSeq?: unknown; data?: unknown }\n if (n.kind !== 'user') continue\n const data = n.data as { time?: unknown; content?: unknown } | null\n if (data === null || typeof data !== 'object') continue\n if (typeof data.time !== 'number' || !Array.isArray(data.content)) continue\n const key = typeof n.key === 'string' ? n.key : undefined\n if (key === undefined) continue\n out.push({ seq: typeof n.anchorSeq === 'number' ? n.anchorSeq : 0, time: data.time, text: userTextOf(data.content), key })\n }\n out.sort((a, b) => a.seq - b.seq)\n return out\n}\n\n/** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */\nfunction anchorKeyOf(m: RailMessage): string | undefined {\n if (typeof m.key === 'string' && m.key !== '') return m.key\n if (typeof m.id === 'string' && m.id !== '') return '13:input-message' + m.id\n return undefined\n}\n\nconst delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\n\n/**\n * Ensure the message node is loaded into the visible window, then scroll to it.\n * loadOlder pages 50 messages at a time (DSH PAGE_MESSAGES), so a jump far back\n * in a long session needs many pages. Report page count through onProgress so\n * the UI can show a loading state instead of appearing frozen.\n */\nasync function jumpToMessage(\n sessionsService: { binding: (id: string) => { session?: { getSnapshot(): unknown; loadOlder(): Promise<unknown>; hasMore?: boolean; loadingOlder?: boolean } } | undefined },\n sessionId: string,\n key: string,\n onProgress?: (pages: number) => void,\n): Promise<boolean> {\n const session = sessionsService.binding(sessionId)?.session\n if (session === undefined) return false\n let pages = 0\n let guard = 0\n while (guard++ < 120) {\n const snapshot = session.getSnapshot() as { chat?: { nodes?: Map<string, unknown> }; hasMore?: boolean; loadingOlder?: boolean } | undefined\n if (snapshot?.chat?.nodes?.get(key) !== undefined) break\n if (snapshot?.hasMore !== true) return false\n if (snapshot.loadingOlder === true) {\n // Another loader owns the current page; wait for it without busy-spinning.\n await delay(50)\n continue\n }\n await session.loadOlder()\n pages++\n // Report progress only on page boundaries, not every loop iteration.\n onProgress?.(pages)\n }\n const scrollport = typeof document !== 'undefined' ? document.querySelector('[data-conversation-scroll]') : null\n const row = scrollport === null ? null : scrollport.querySelector(`[data-chat-anchor-key=\"${CSS.escape(key)}\"]`)\n if (row === null) return false\n const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n row.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'center' })\n return true\n}\n\n// ---- component ----\ninterface TimelineRailProps {\n useProjection: (key: string) => { messages?: unknown[] } | undefined\n sessionId?: SessionId\n sessionsService: ISessions\n}\n\n/** Resolve copy for the current UI language (document lang, DSH sets zh/en). */\nfunction langStrings(): Record<string, string> {\n const lang = typeof document !== 'undefined' ? (document.documentElement.lang || 'zh').toLowerCase() : 'zh'\n return STRINGS[lang.startsWith('zh') ? 'zh' : 'en']\n}\n\nfunction TimelineRail({ useProjection, sessionId, sessionsService }: TimelineRailProps): ReactNode {\n const t = langStrings()\n const projected = useProjection('chatRail')\n const session = sessionId === undefined ? undefined : (sessionsService.binding(sessionId) as { session?: { subscribe(cb: () => void): () => void; getSnapshot(): unknown } } | undefined)?.session\n const fallbackStore = session === undefined ? NOOP_STORE : session\n const nodeSnapshot = useSyncExternalStore(\n (cb) => fallbackStore.subscribe(cb),\n () => fallbackStore.getSnapshot(),\n )\n\n let messages: RailMessage[] = []\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) {\n messages = projected.messages.map(normalize).filter((m): m is RailMessage => m !== null)\n }\n if (messages.length === 0) {\n messages = collectFromNodes(nodeSnapshot)\n }\n\n const [activeIndex, setActiveIndex] = useState(-1)\n const [show, setShow] = useState(false)\n const [jumping, setJumping] = useState(false)\n // Hover full-content panel: index of the hovered item + fixed position.\n // The tip only triggers after the expand animation settles — in the\n // collapsed state the item rect is only 36px wide, so a tip positioned\n // there would be wrong once the rail expands.\n const [tip, setTip] = useState<{ index: number; x: number; y: number } | null>(null)\n const navRef = useRef<HTMLDivElement | null>(null)\n // True only after the expand animation has fully settled; the width\n // transition takes ~250ms after `show` flips, and item rects are only\n // stable once it finishes. Tip positioning must wait for this.\n const expandedRef = useRef(false)\n\n /** Position the tip against the item's CURRENT (post-expand) rect. */\n const positionTip = (index: number) => {\n if (index < 0) return\n const el = navRef.current?.querySelector<HTMLElement>(`[data-crl-index=\"${index}\"]`)\n if (el === null || el === undefined) return\n const rect = el.getBoundingClientRect()\n setTip({ index, x: rect.left - 12, y: rect.top })\n }\n\n /** Handle item hover: the collapsed state only expands the rail; the tip\n * only triggers once the expand animation has settled (item rects are then\n * stable, so the tip is positioned correctly). */\n const handleItemEnter = (index: number) => {\n if (!expandedRef.current) {\n setShow(true)\n return\n }\n positionTip(index)\n }\n\n const handleItemLeave = (index: number) => {\n setTip((prev) => (prev?.index === index ? null : prev))\n }\n\n // Track the settled expand state: when `show` flips true, wait for the width\n // transition to end before allowing tip triggers; when it flips false, the\n // rail is collapsing — treat as unexpanded immediately.\n useEffect(() => {\n if (!show) {\n expandedRef.current = false\n return\n }\n const el = navRef.current\n if (el === null) return\n const onTransitionEnd = (e: TransitionEvent) => {\n if (e.propertyName !== 'width') return\n expandedRef.current = true\n el.removeEventListener('transitionend', onTransitionEnd)\n }\n el.addEventListener('transitionend', onTransitionEnd)\n return () => el.removeEventListener('transitionend', onTransitionEnd)\n }, [show])\n\n /** Full text of a rail message from the loaded chat nodes (uncapped),\n * falling back to the projection preview when the node is not mounted. */\n const fullTextOf = (m: RailMessage): string => {\n const key = anchorKeyOf(m)\n const nodes = (nodeSnapshot as { chat?: { nodes?: Map<string, { data?: { content?: unknown } }> } } | undefined)?.chat?.nodes\n const node = key === undefined ? undefined : nodes?.get(key)\n const content = node?.data?.content\n if (Array.isArray(content)) {\n let out = ''\n for (const block of content) {\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\n && typeof (block as { text?: unknown }).text === 'string') {\n out += (block as { text: string }).text\n }\n }\n const full = out.trim()\n if (full !== '') return full\n }\n return m.text\n }\n\n // Background full-history load: follow the runtime's authoritative hasMore\n // flag, but STOP as soon as the projection delivers.\n useEffect(() => {\n if (session === undefined) return\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\n let cancelled = false\n const run = async () => {\n let guard = 0\n while (!cancelled && guard++ < 120) {\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\n const snap = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean }\n if (snap?.hasMore !== true) return\n if (snap.loadingOlder === true) { await delay(50); continue }\n await (session as unknown as { loadOlder(): Promise<unknown> }).loadOlder()\n }\n }\n run().catch(() => {})\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sessionId, session === undefined ? 'none' : 'ready', Array.isArray(projected?.messages) && projected.messages.length > 0 ? 'have' : 'none'])\n\n // Track the reading position (active item): nearest row to the 40% viewport line.\n useEffect(() => {\n if (messages.length === 0) return\n const indexByKey = new Map<string, number>()\n for (let i = 0; i < messages.length; i++) {\n const key = anchorKeyOf(messages[i])\n if (key !== undefined) indexByKey.set(key, i)\n }\n const updateActive = () => {\n const sp = document.querySelector('[data-conversation-scroll]')\n if (sp === null) return\n const rect = sp.getBoundingClientRect()\n if (rect.height === 0) return\n const line = rect.top + rect.height * 0.4\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"]')\n let best = -1\n let bestDist = Infinity\n for (const row of rows) {\n const key = row.getAttribute('data-chat-anchor-key')\n if (key === null) continue\n const idx = indexByKey.get(key) ?? -1\n if (idx === -1) continue\n const r = row.getBoundingClientRect()\n const dist = Math.abs(r.top + r.height / 2 - line)\n if (dist < bestDist) { bestDist = dist; best = idx }\n }\n setActiveIndex(best)\n }\n updateActive()\n const el = document.querySelector('[data-conversation-scroll]')\n let scrollTimer: ReturnType<typeof setTimeout> | null = null\n const onScroll = () => {\n if (scrollTimer !== null) return\n scrollTimer = setTimeout(() => { scrollTimer = null; updateActive() }, 60)\n }\n el?.addEventListener('scroll', onScroll, { passive: true })\n const timer = window.setInterval(updateActive, 2000)\n return () => {\n el?.removeEventListener('scroll', onScroll)\n window.clearInterval(timer)\n if (scrollTimer !== null) clearTimeout(scrollTimer)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sessionId, messages.length])\n\n // Scroll the rail so the active item stays centered in its visible area:\n // follows the conversation's reading position (and re-centers after a\n // session switch, where activeIndex is recomputed for the new session).\n useEffect(() => {\n if (activeIndex < 0) return\n const el = navRef.current\n if (el === null) return\n const item = el.querySelector<HTMLElement>(`[data-crl-index=\"${activeIndex}\"]`)\n if (item === null) return\n const target = item.offsetTop - el.clientHeight / 2 + item.clientHeight / 2\n el.scrollTo({ top: Math.max(0, target), behavior: 'smooth' })\n }, [activeIndex])\n\n if (sessionId === undefined || messages.length < 2) return null\n\n const items = messages.map((m, i) => {\n const key = anchorKeyOf(m)\n return createElement('button', {\n type: 'button',\n key: m.seq,\n 'data-crl-index': String(i),\n className: S.item + (activeIndex === i ? ` ${S.itemActive}` : ''),\n 'aria-label': `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,\n 'aria-current': activeIndex === i ? 'location' : undefined,\n disabled: jumping,\n onClick: () => {\n if (key === undefined || jumping) return\n setJumping(true)\n void jumpToMessage(sessionsService as never, sessionId as string, key).finally(() => setJumping(false))\n },\n onMouseEnter: () => handleItemEnter(i),\n onMouseLeave: () => handleItemLeave(i),\n children: [\n createElement('span', { className: S.num }, `#${i + 1}`),\n createElement('span', { className: S.title }, m.text === '' ? t.noText : m.text),\n createElement('span', { className: S.time }, relativeTime(m.time, t)),\n createElement('span', { className: S.ind, 'aria-hidden': true },\n createElement('span', { className: S.line })),\n ],\n })\n })\n\n return createPortal(\n // Fragment: the nav (transformed) and the tip must be siblings — a fixed\n // child inside a transformed ancestor is positioned relative to that\n // ancestor, breaking the viewport coordinates the tip uses.\n [createElement('div', {\n ref: navRef,\n className: S.nav + (show ? ` ${S.navShow}` : ''),\n role: 'navigation',\n 'aria-label': t.railLabel,\n onMouseEnter: () => setShow(true),\n onMouseLeave: () => setShow(false),\n children: [\n jumping ? createElement('div', { className: S.loading, key: 'loading' },\n createElement('span', { className: S.loadingLabel }, t.loading)) : null,\n ...items,\n ],\n }),\n // Full-content panel: anchored to the left of the hovered item.\n tip !== null && tip.index >= 0 && tip.index < messages.length\n ? createElement('div', {\n className: S.tip,\n style: { left: `${tip.x}px`, top: `${tip.y}px`, transform: 'translateX(-100%)' },\n }, fullTextOf(messages[tip.index]) || t.noText)\n : null],\n document.body,\n )\n}\n\nfunction apply(ctx: ClientContext): void {\n ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({\n name: 'conversation.input.dock',\n id: 'chat-rail',\n order: 40,\n inject: () => ({ sessionsService: ctx.sessions }),\n }, TimelineRail))\n}\n\nexport { apply, TimelineRail }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgCA,MAAa,SAAS,CAAC,SAAS,UAAU;EAI1C,MAAM,UAAoD;GACxD,IAAI;IACF,WAAW;IACX,UAAU;IACV,QAAQ;IACR,UAAU;IACV,SAAS;IACT,aAAa;IACb,aAAa;IACb,WAAW;IACX,UAAU;GACZ;GACA,IAAI;IACF,WAAW;IACX,UAAU;IACV,QAAQ;IACR,UAAU;IACV,SAAS;IACT,aAAa;IACb,aAAa;IACb,WAAW;IACX,UAAU;GACZ;EACF;;EAGA,SAAS,aAAa,IAAY,GAAmC;GACnE,MAAM,OAAO,KAAK,IAAI,IAAI;GAC1B,IAAI,OAAO,KAAQ,OAAO,EAAE;GAC5B,IAAI,OAAO,MAAW,OAAO,EAAE,YAAY,QAAQ,OAAO,OAAO,KAAK,MAAM,OAAO,GAAM,CAAC,CAAC;GAC3F,IAAI,OAAO,OAAY,OAAO,EAAE,UAAU,QAAQ,OAAO,OAAO,KAAK,MAAM,OAAO,IAAS,CAAC,CAAC;GAC7F,IAAI,OAAO,QAAgB,OAAO,EAAE,SAAS,QAAQ,OAAO,OAAO,KAAK,MAAM,OAAO,KAAU,CAAC,CAAC;GACjG,MAAM,IAAI,IAAI,KAAK,EAAE;GACrB,OAAO,GAAG,EAAE,SAAS,IAAI,EAAE,GAAG,EAAE,QAAQ;EAC1C;EAQA,MAAM,MAAM;GACV;GACA;GACA;GACA;GACA;GACA;GAIA;GACA;GACA;GACA;GACA;GAMA;GACA;GACA;GACA;GACA;GACA;GAMA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAEA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAEA;GACA;GACA;EACF,CAAC,CAAC,KAAK,EAAE;EAET,MAAM,WAAW;EACjB,IAAI,OAAO,aAAa,eAAe,SAAS,cAAc,0BAA0B,SAAS,GAAG,MAAM,MAAM;GAC9G,MAAM,MAAM,SAAS,cAAc,OAAO;GAC1C,IAAI,QAAQ,SAAS;GACrB,IAAI,QAAQ,YAAY;GACxB,IAAI,cAAc;GAClB,SAAS,KAAK,YAAY,GAAG;EAC/B;EAEA,MAAM,IAAI;GACR,KAAK;GACL,SAAS;GACT,MAAM;GACN,YAAY;GACZ,OAAO;GACP,KAAK;GACL,MAAM;GACN,KAAK;GACL,MAAM;GACN,SAAS;GACT,cAAc;GACd,KAAK;EACP;EAGA,MAAM,aAAa;GAAE,mBAAmB,KAAA;GAAW,uBAAuB,CAAC;EAAE;;EAW7E,SAAS,WAAW,SAA0B;GAC5C,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;GACpC,IAAI,MAAM;GACV,KAAK,MAAM,SAAS,SAClB,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAa,MAA6B,SAAS,UACrF,OAAQ,MAA6B,SAAS,UACjD,OAAQ,MAA2B;GAGvC,OAAO,IAAI,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE;EAC/B;;EAGA,SAAS,UAAU,GAAgC;GACjD,IAAI,MAAM,QAAQ,OAAO,MAAM,UAAU,OAAO;GAChD,MAAM,IAAI;GACV,IAAI,OAAO,EAAE,QAAQ,UAAU,OAAO;GAGtC,MAAM,OAAO,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU;GAC/F,OAAO;IACL,KAAK,EAAE;IACP,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;IAC5C;IACA,GAAI,OAAO,EAAE,QAAQ,WAAW,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC;IAClD,GAAI,OAAO,EAAE,OAAO,WAAW,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;GACjD;EACF;;EAGA,SAAS,iBAAiB,UAAkC;GAC1D,MAAM,MAAqB,CAAC;GAC5B,IAAI,aAAa,KAAA,KAAc,SAAgC,SAAS,KAAA,GAAW,OAAO;GAC1F,MAAM,OAAQ,SAAyD;GACvE,IAAI,CAAC,KAAK,OAAO,OAAO;GACxB,KAAK,MAAM,QAAQ,KAAK,MAAM,OAAO,GAAG;IACtC,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU;IAC/C,MAAM,IAAI;IACV,IAAI,EAAE,SAAS,QAAQ;IACvB,MAAM,OAAO,EAAE;IACf,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU;IAC/C,IAAI,OAAO,KAAK,SAAS,YAAY,CAAC,MAAM,QAAQ,KAAK,OAAO,GAAG;IACnE,MAAM,MAAM,OAAO,EAAE,QAAQ,WAAW,EAAE,MAAM,KAAA;IAChD,IAAI,QAAQ,KAAA,GAAW;IACvB,IAAI,KAAK;KAAE,KAAK,OAAO,EAAE,cAAc,WAAW,EAAE,YAAY;KAAG,MAAM,KAAK;KAAM,MAAM,WAAW,KAAK,OAAO;KAAG;IAAI,CAAC;GAC3H;GACA,IAAI,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG;GAChC,OAAO;EACT;;EAGA,SAAS,YAAY,GAAoC;GACvD,IAAI,OAAO,EAAE,QAAQ,YAAY,EAAE,QAAQ,IAAI,OAAO,EAAE;GACxD,IAAI,OAAO,EAAE,OAAO,YAAY,EAAE,OAAO,IAAI,OAAO,qBAAqB,EAAE;EAE7E;EAEA,MAAM,SAAS,OAAe,IAAI,SAAe,YAAY,WAAW,SAAS,EAAE,CAAC;;;;;;;EAQpF,eAAe,cACb,iBACA,WACA,KACA,YACkB;GAClB,MAAM,UAAU,gBAAgB,QAAQ,SAAS,CAAC,EAAE;GACpD,IAAI,YAAY,KAAA,GAAW,OAAO;GAClC,IAAI,QAAQ;GACZ,IAAI,QAAQ;GACZ,OAAO,UAAU,KAAK;IACpB,MAAM,WAAW,QAAQ,YAAY;IACrC,IAAI,UAAU,MAAM,OAAO,IAAI,GAAG,MAAM,KAAA,GAAW;IACnD,IAAI,UAAU,YAAY,MAAM,OAAO;IACvC,IAAI,SAAS,iBAAiB,MAAM;KAElC,MAAM,MAAM,EAAE;KACd;IACF;IACA,MAAM,QAAQ,UAAU;IACxB;IAEA,aAAa,KAAK;GACpB;GACA,MAAM,aAAa,OAAO,aAAa,cAAc,SAAS,cAAc,4BAA4B,IAAI;GAC5G,MAAM,MAAM,eAAe,OAAO,OAAO,WAAW,cAAc,0BAA0B,IAAI,OAAO,GAAG,EAAE,GAAG;GAC/G,IAAI,QAAQ,MAAM,OAAO;GACzB,MAAM,gBAAgB,OAAO,OAAO,eAAe,cAAc,OAAO,WAAW,kCAAkC,CAAC,CAAC;GACvH,IAAI,eAAe;IAAE,UAAU,gBAAgB,SAAS;IAAU,OAAO;GAAS,CAAC;GACnF,OAAO;EACT;;EAUA,SAAS,cAAsC;GAC7C,MAAM,OAAO,OAAO,aAAa,eAAe,SAAS,gBAAgB,QAAQ,KAAA,CAAM,YAAY,IAAI;GACvG,OAAO,QAAQ,KAAK,WAAW,IAAI,IAAI,OAAO;EAChD;EAEA,SAAS,aAAa,EAAE,eAAe,WAAW,mBAAiD;GACjG,MAAM,IAAI,YAAY;GACtB,MAAM,YAAY,cAAc,UAAU;GAC1C,MAAM,UAAU,cAAc,KAAA,IAAY,KAAA,IAAa,gBAAgB,QAAQ,SAAS,CAAC,EAAkG;GAC3L,MAAM,gBAAgB,YAAY,KAAA,IAAY,aAAa;GAC3D,MAAM,gBAAA,GAAeA,MAAAA,qBAAAA,EAClB,OAAO,cAAc,UAAU,EAAE,SAC5B,cAAc,YAAY,CAClC;GAEA,IAAI,WAA0B,CAAC;GAC/B,IAAI,MAAM,QAAQ,WAAW,QAAQ,KAAK,UAAU,SAAS,SAAS,GACpE,WAAW,UAAU,SAAS,IAAI,SAAS,CAAC,CAAC,QAAQ,MAAwB,MAAM,IAAI;GAEzF,IAAI,SAAS,WAAW,GACtB,WAAW,iBAAiB,YAAY;GAG1C,MAAM,CAAC,aAAa,mBAAA,GAAkBC,MAAAA,SAAAA,CAAS,EAAE;GACjD,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,KAAK;GACtC,MAAM,CAAC,SAAS,eAAA,GAAcA,MAAAA,SAAAA,CAAS,KAAK;GAK5C,MAAM,CAAC,KAAK,WAAA,GAAUA,MAAAA,SAAAA,CAAyD,IAAI;GACnF,MAAM,UAAA,GAASC,MAAAA,OAAAA,CAA8B,IAAI;GAIjD,MAAM,eAAA,GAAcA,MAAAA,OAAAA,CAAO,KAAK;;GAGhC,MAAM,eAAe,UAAkB;IACrC,IAAI,QAAQ,GAAG;IACf,MAAM,KAAK,OAAO,SAAS,cAA2B,oBAAoB,MAAM,GAAG;IACnF,IAAI,OAAO,QAAQ,OAAO,KAAA,GAAW;IACrC,MAAM,OAAO,GAAG,sBAAsB;IACtC,OAAO;KAAE;KAAO,GAAG,KAAK,OAAO;KAAI,GAAG,KAAK;IAAI,CAAC;GAClD;;;;GAKA,MAAM,mBAAmB,UAAkB;IACzC,IAAI,CAAC,YAAY,SAAS;KACxB,QAAQ,IAAI;KACZ;IACF;IACA,YAAY,KAAK;GACnB;GAEA,MAAM,mBAAmB,UAAkB;IACzC,QAAQ,SAAU,MAAM,UAAU,QAAQ,OAAO,IAAK;GACxD;GAKA,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,CAAC,MAAM;KACT,YAAY,UAAU;KACtB;IACF;IACA,MAAM,KAAK,OAAO;IAClB,IAAI,OAAO,MAAM;IACjB,MAAM,mBAAmB,MAAuB;KAC9C,IAAI,EAAE,iBAAiB,SAAS;KAChC,YAAY,UAAU;KACtB,GAAG,oBAAoB,iBAAiB,eAAe;IACzD;IACA,GAAG,iBAAiB,iBAAiB,eAAe;IACpD,aAAa,GAAG,oBAAoB,iBAAiB,eAAe;GACtE,GAAG,CAAC,IAAI,CAAC;;;GAIT,MAAM,cAAc,MAA2B;IAC7C,MAAM,MAAM,YAAY,CAAC;IACzB,MAAM,QAAS,cAAmG,MAAM;IAExH,MAAM,WADO,QAAQ,KAAA,IAAY,KAAA,IAAY,OAAO,IAAI,GAAG,EAAA,EACrC,MAAM;IAC5B,IAAI,MAAM,QAAQ,OAAO,GAAG;KAC1B,IAAI,MAAM;KACV,KAAK,MAAM,SAAS,SAClB,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAa,MAA6B,SAAS,UACrF,OAAQ,MAA6B,SAAS,UACjD,OAAQ,MAA2B;KAGvC,MAAM,OAAO,IAAI,KAAK;KACtB,IAAI,SAAS,IAAI,OAAO;IAC1B;IACA,OAAO,EAAE;GACX;GAIA,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,YAAY,KAAA,GAAW;IAC3B,IAAI,MAAM,QAAQ,WAAW,QAAQ,KAAK,UAAU,SAAS,SAAS,GAAG;IACzE,IAAI,YAAY;IAChB,MAAM,MAAM,YAAY;KACtB,IAAI,QAAQ;KACZ,OAAO,CAAC,aAAa,UAAU,KAAK;MAClC,IAAI,MAAM,QAAQ,WAAW,QAAQ,KAAK,UAAU,SAAS,SAAS,GAAG;MACzE,MAAM,OAAO,QAAQ,YAAY;MACjC,IAAI,MAAM,YAAY,MAAM;MAC5B,IAAI,KAAK,iBAAiB,MAAM;OAAE,MAAM,MAAM,EAAE;OAAG;MAAS;MAC5D,MAAO,QAAyD,UAAU;KAC5E;IACF;IACA,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC;IACpB,aAAa;KAAE,YAAY;IAAK;GAElC,GAAG;IAAC;IAAW,YAAY,KAAA,IAAY,SAAS;IAAS,MAAM,QAAQ,WAAW,QAAQ,KAAK,UAAU,SAAS,SAAS,IAAI,SAAS;GAAM,CAAC;GAG/I,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,SAAS,WAAW,GAAG;IAC3B,MAAM,6BAAa,IAAI,IAAoB;IAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;KACxC,MAAM,MAAM,YAAY,SAAS,EAAE;KACnC,IAAI,QAAQ,KAAA,GAAW,WAAW,IAAI,KAAK,CAAC;IAC9C;IACA,MAAM,qBAAqB;KACzB,MAAM,KAAK,SAAS,cAAc,4BAA4B;KAC9D,IAAI,OAAO,MAAM;KACjB,MAAM,OAAO,GAAG,sBAAsB;KACtC,IAAI,KAAK,WAAW,GAAG;KACvB,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS;KACtC,MAAM,OAAO,GAAG,iBAAiB,8CAA4C;KAC7E,IAAI,OAAO;KACX,IAAI,WAAW;KACf,KAAK,MAAM,OAAO,MAAM;MACtB,MAAM,MAAM,IAAI,aAAa,sBAAsB;MACnD,IAAI,QAAQ,MAAM;MAClB,MAAM,MAAM,WAAW,IAAI,GAAG,KAAK;MACnC,IAAI,QAAQ,IAAI;MAChB,MAAM,IAAI,IAAI,sBAAsB;MACpC,MAAM,OAAO,KAAK,IAAI,EAAE,MAAM,EAAE,SAAS,IAAI,IAAI;MACjD,IAAI,OAAO,UAAU;OAAE,WAAW;OAAM,OAAO;MAAI;KACrD;KACA,eAAe,IAAI;IACrB;IACA,aAAa;IACb,MAAM,KAAK,SAAS,cAAc,4BAA4B;IAC9D,IAAI,cAAoD;IACxD,MAAM,iBAAiB;KACrB,IAAI,gBAAgB,MAAM;KAC1B,cAAc,iBAAiB;MAAE,cAAc;MAAM,aAAa;KAAE,GAAG,EAAE;IAC3E;IACA,IAAI,iBAAiB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;IAC1D,MAAM,QAAQ,OAAO,YAAY,cAAc,GAAI;IACnD,aAAa;KACX,IAAI,oBAAoB,UAAU,QAAQ;KAC1C,OAAO,cAAc,KAAK;KAC1B,IAAI,gBAAgB,MAAM,aAAa,WAAW;IACpD;GAEF,GAAG,CAAC,WAAW,SAAS,MAAM,CAAC;GAK/B,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,cAAc,GAAG;IACrB,MAAM,KAAK,OAAO;IAClB,IAAI,OAAO,MAAM;IACjB,MAAM,OAAO,GAAG,cAA2B,oBAAoB,YAAY,GAAG;IAC9E,IAAI,SAAS,MAAM;IACnB,MAAM,SAAS,KAAK,YAAY,GAAG,eAAe,IAAI,KAAK,eAAe;IAC1E,GAAG,SAAS;KAAE,KAAK,KAAK,IAAI,GAAG,MAAM;KAAG,UAAU;IAAS,CAAC;GAC9D,GAAG,CAAC,WAAW,CAAC;GAEhB,IAAI,cAAc,KAAA,KAAa,SAAS,SAAS,GAAG,OAAO;GAE3D,MAAM,QAAQ,SAAS,KAAK,GAAG,MAAM;IACnC,MAAM,MAAM,YAAY,CAAC;IACzB,QAAA,GAAOC,MAAAA,cAAAA,CAAc,UAAU;KAC7B,MAAM;KACN,KAAK,EAAE;KACP,kBAAkB,OAAO,CAAC;KAC1B,WAAW,EAAE,QAAQ,gBAAgB,IAAI,IAAI,EAAE,eAAe;KAC9D,cAAc,GAAG,EAAE,SAAS,IAAI,EAAE,KAAK,MAAM,GAAG,EAAE,KAAK,EAAE,OAAO,IAAI,EAAE,SAAS;KAC/E,gBAAgB,gBAAgB,IAAI,aAAa,KAAA;KACjD,UAAU;KACV,eAAe;MACb,IAAI,QAAQ,KAAA,KAAa,SAAS;MAClC,WAAW,IAAI;MACf,cAAmB,iBAA0B,WAAqB,GAAG,CAAC,CAAC,cAAc,WAAW,KAAK,CAAC;KACxG;KACA,oBAAoB,gBAAgB,CAAC;KACrC,oBAAoB,gBAAgB,CAAC;KACrC,UAAU;OACRA,GAAAA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,EAAE,IAAI,GAAG,IAAI,IAAI,GAAG;OACvDA,GAAAA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,EAAE,MAAM,GAAG,EAAE,SAAS,KAAK,EAAE,SAAS,EAAE,IAAI;OAC/EA,GAAAA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,EAAE,KAAK,GAAG,aAAa,EAAE,MAAM,CAAC,CAAC;OACpEA,GAAAA,MAAAA,cAAAA,CAAc,QAAQ;OAAE,WAAW,EAAE;OAAK,eAAe;MAAK,IAAA,GAC5DA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC;KAChD;IACF,CAAC;GACH,CAAC;GAED,QAAA,GAAOC,UAAAA,aAAAA,CAIL,EAAA,GAACD,MAAAA,cAAAA,CAAc,OAAO;IACpB,KAAK;IACL,WAAW,EAAE,OAAO,OAAO,IAAI,EAAE,YAAY;IAC7C,MAAM;IACN,cAAc,EAAE;IAChB,oBAAoB,QAAQ,IAAI;IAChC,oBAAoB,QAAQ,KAAK;IACjC,UAAU,CACR,WAAA,GAAUA,MAAAA,cAAAA,CAAc,OAAO;KAAE,WAAW,EAAE;KAAS,KAAK;IAAU,IAAA,GACpEA,MAAAA,cAAAA,CAAc,QAAQ,EAAE,WAAW,EAAE,aAAa,GAAG,EAAE,OAAO,CAAC,IAAI,MACrE,GAAG,KACL;GACF,CAAC,GAED,QAAQ,QAAQ,IAAI,SAAS,KAAK,IAAI,QAAQ,SAAS,UAAA,GACnDA,MAAAA,cAAAA,CAAc,OAAO;IACnB,WAAW,EAAE;IACb,OAAO;KAAE,MAAM,GAAG,IAAI,EAAE;KAAK,KAAK,GAAG,IAAI,EAAE;KAAK,WAAW;IAAoB;GACjF,GAAG,WAAW,SAAS,IAAI,MAAM,KAAK,EAAE,MAAM,IAC9C,IAAI,GACR,SAAS,IACX;EACF;EAEA,SAAS,MAAM,KAA0B;GACvC,IAAI,MAAM,OAAO,iCAAiC,IAAI,MAAM,SAAS;IACnE,MAAM;IACN,IAAI;IACJ,OAAO;IACP,eAAe,EAAE,iBAAiB,IAAI,SAAS;GACjD,GAAG,YAAY,CAAC;EAClB"}
|
|
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 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 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_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 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 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/** 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 ...(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), 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 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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"}
|
package/package.json
CHANGED
|
@@ -1,88 +1,89 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@max-null/dsh-chat-rail",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "画卷式消息导航栏:右侧竖条 hover 展开画卷,scroll-spy 高亮居中跟随,编号+时间+完整预览,与 better-sidebar 双面板动画同步避让",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.mjs",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/Max-Null/dsh-chat-rail.git"
|
|
10
|
-
},
|
|
11
|
-
"homepage": "https://github.com/Max-Null/dsh-chat-rail#readme",
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/Max-Null/dsh-chat-rail/issues"
|
|
14
|
-
},
|
|
15
|
-
"exports": {
|
|
16
|
-
".": "./lib/index.mjs",
|
|
17
|
-
"./client": "./lib/client.js",
|
|
18
|
-
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
19
|
-
"./package.json": "./package.json"
|
|
20
|
-
},
|
|
21
|
-
"files": [
|
|
22
|
-
"lib",
|
|
23
|
-
"cordis.patch.yml",
|
|
24
|
-
"README.md",
|
|
25
|
-
"LICENSE",
|
|
26
|
-
"docs/shots"
|
|
27
|
-
],
|
|
28
|
-
"dsh": {
|
|
29
|
-
"bundle": {
|
|
30
|
-
"patch": "./cordis.patch.yml"
|
|
31
|
-
},
|
|
32
|
-
"client": {
|
|
33
|
-
"inject": [
|
|
34
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
35
|
-
"@deepseek-ai/dsh-client-locale",
|
|
36
|
-
"@deepseek-ai/dsh-client-ui-slots",
|
|
37
|
-
"@deepseek-ai/dsh-client-ui-conversation"
|
|
38
|
-
],
|
|
39
|
-
"platform": "web"
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"scripts": {
|
|
43
|
-
"build": "tsdown",
|
|
44
|
-
"typecheck": "tsc --noEmit",
|
|
45
|
-
"watch": "tsdown --watch",
|
|
46
|
-
"test": "node --import tsx/esm --test tests/**/*.spec.ts"
|
|
47
|
-
},
|
|
48
|
-
"license": "MIT",
|
|
49
|
-
"keywords": [
|
|
50
|
-
"dsh",
|
|
51
|
-
"dsh-plugin",
|
|
52
|
-
"deepseek-harness",
|
|
53
|
-
"ssid",
|
|
54
|
-
"conversation",
|
|
55
|
-
"navigation",
|
|
56
|
-
"timeline",
|
|
57
|
-
"rail"
|
|
58
|
-
],
|
|
59
|
-
"peerDependencies": {
|
|
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",
|
|
63
|
-
"react": "^18.2.0",
|
|
64
|
-
"react-dom": "^18.2.0"
|
|
65
|
-
},
|
|
66
|
-
"peerDependenciesMeta": {
|
|
67
|
-
"dsh-better-sidebar": {
|
|
68
|
-
"optional": true
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
"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",
|
|
77
|
-
"@types/node": "^22.0.0",
|
|
78
|
-
"@types/react": "~18.3.1",
|
|
79
|
-
"@types/react-dom": "^18.3.7",
|
|
80
|
-
"cordis": "^4.0.0-rc.7",
|
|
81
|
-
"dsh-better-sidebar": "^0.13.0",
|
|
82
|
-
"react": "^18.2.0",
|
|
83
|
-
"react-dom": "^18.2.0",
|
|
84
|
-
"tsdown": "^0.22.2",
|
|
85
|
-
"tsx": "^4.0.0",
|
|
86
|
-
"typescript": "^5.6.0"
|
|
87
|
-
|
|
88
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@max-null/dsh-chat-rail",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "画卷式消息导航栏:右侧竖条 hover 展开画卷,scroll-spy 高亮居中跟随,编号+时间+完整预览,与 better-sidebar 双面板动画同步避让",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.mjs",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Max-Null/dsh-chat-rail.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Max-Null/dsh-chat-rail#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Max-Null/dsh-chat-rail/issues"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./lib/index.mjs",
|
|
17
|
+
"./client": "./lib/client.js",
|
|
18
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lib",
|
|
23
|
+
"cordis.patch.yml",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"docs/shots"
|
|
27
|
+
],
|
|
28
|
+
"dsh": {
|
|
29
|
+
"bundle": {
|
|
30
|
+
"patch": "./cordis.patch.yml"
|
|
31
|
+
},
|
|
32
|
+
"client": {
|
|
33
|
+
"inject": [
|
|
34
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
35
|
+
"@deepseek-ai/dsh-client-locale",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
38
|
+
],
|
|
39
|
+
"platform": "web"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"watch": "tsdown --watch",
|
|
46
|
+
"test": "node --import tsx/esm --test tests/**/*.spec.ts"
|
|
47
|
+
},
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"keywords": [
|
|
50
|
+
"dsh",
|
|
51
|
+
"dsh-plugin",
|
|
52
|
+
"deepseek-harness",
|
|
53
|
+
"ssid",
|
|
54
|
+
"conversation",
|
|
55
|
+
"navigation",
|
|
56
|
+
"timeline",
|
|
57
|
+
"rail"
|
|
58
|
+
],
|
|
59
|
+
"peerDependencies": {
|
|
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",
|
|
63
|
+
"react": "^18.2.0",
|
|
64
|
+
"react-dom": "^18.2.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"dsh-better-sidebar": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"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",
|
|
77
|
+
"@types/node": "^22.0.0",
|
|
78
|
+
"@types/react": "~18.3.1",
|
|
79
|
+
"@types/react-dom": "^18.3.7",
|
|
80
|
+
"cordis": "^4.0.0-rc.7",
|
|
81
|
+
"dsh-better-sidebar": "^0.13.0",
|
|
82
|
+
"react": "^18.2.0",
|
|
83
|
+
"react-dom": "^18.2.0",
|
|
84
|
+
"tsdown": "^0.22.2",
|
|
85
|
+
"tsx": "^4.0.0",
|
|
86
|
+
"typescript": "^5.6.0",
|
|
87
|
+
"unrun": "^0.3.1"
|
|
88
|
+
}
|
|
89
|
+
}
|