@lament_z/dsh-client-ui-chat-timeline 0.1.0 → 0.2.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":[],"sources":["../src/client/directory.ts","../src/client/dom.ts","../src/client/styles.ts","../src/client/rail.tsx","../src/client/source.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Timeline directory derivation — the pure core of the plugin.\n *\n * Turns the conversation snapshot's ordered node list into one tick per human\n * input (`user` / `steering`), each carrying a trimmed user preview plus the\n * assistant reply preview in one of three states (text / running / empty).\n * Preview budgeting replicates the ZCode TurnNavigator: at most two\n * paragraphs, whitespace-collapsed, truncated to 220 characters.\n *\n * No DOM, no React, no dsh runtime imports — everything here is plain data so\n * vitest can cover it without fixtures beyond plain objects.\n */\n\n/** Minimal structural shape of a conversation node this module needs. */\nexport interface TimelineNodeLike {\n kind: string\n seq: number\n /** Unix epoch ms when present. */\n time?: number\n /** User/steering content blocks (each `{ kind, text? }`-like). */\n content?: readonly unknown[]\n /** Assistant content blocks (each `{ kind, text? }`-like). */\n blocks?: readonly unknown[]\n}\n\n/** One tick in the timeline directory. */\nexport interface TimelineItem {\n /** Stable React key (`n<seq>`). */\n readonly key: string\n /** Seq of the anchoring human node. */\n readonly seq: number\n /** Anchor time (epoch ms); 0 when unknown. */\n readonly time: number\n /** Trimmed human input preview. */\n readonly userPreview: string\n /** True when the human node carried no extractable text. */\n readonly userFallback: boolean\n /** Trimmed assistant reply preview. */\n readonly assistantPreview: string\n /** Which assistant preview state applies. */\n readonly assistantKind: 'text' | 'running' | 'empty'\n /** True for the last tick while the session is still running. */\n readonly running: boolean\n}\n\n/** Copy labels the derivation needs (kept out of the pure math for testability). */\nexport interface TimelineLabels {\n readonly userFallback: string\n readonly assistantEmpty: string\n readonly assistantRunning: string\n}\n\n/** Preview budget: at most two paragraphs and 220 characters (ZCode parity). */\nexport const MAX_PREVIEW_CHARS = 220\nexport const MAX_PREVIEW_PARAGRAPHS = 2\n\n/** Human node kinds that anchor a tick. */\nconst HUMAN_KINDS = new Set(['user', 'steering'])\n\n/** Extract concatenated text from a ContentBlock-like list.\n * dsh-llm content blocks switch on `type`; assistant blocks use `kind` — accept both. */\nexport function blocksText(blocks: readonly unknown[] | undefined): string {\n if (!Array.isArray(blocks)) return ''\n const parts: string[] = []\n for (const block of blocks) {\n if (block !== null && typeof block === 'object') {\n const record = block as { kind?: unknown; type?: unknown; text?: unknown }\n if (record.kind === 'text' || record.type === 'text') {\n if (typeof record.text === 'string' && record.text.length > 0) parts.push(record.text)\n }\n }\n }\n return parts.join('\\n\\n')\n}\n\n/** Collapse each paragraph's whitespace and drop empties (ZCode u5e parity). */\nexport function collapseParagraphs(text: string, maxParagraphs: number): string[] {\n return text\n .trim()\n .split(/\\n\\s*\\n/u)\n .map((paragraph) => paragraph.replace(/\\s+/gu, ' ').trim())\n .filter(Boolean)\n .slice(0, Math.max(1, maxParagraphs))\n}\n\n/** Truncate to the budget with an ellipsis, never below the floor (ZCode d5e parity). */\nexport function truncatePreview(text: string, maxChars: number, floor = 8): string {\n const budget = Math.max(floor, maxChars)\n if (text.length <= budget) return text\n return `${text.slice(0, budget - 3).trimEnd()}...`\n}\n\n/** Build one preview string from raw texts under the shared budget. */\nexport function buildPreview(texts: readonly string[], fallback: string): string {\n const paragraphs = collapseParagraphs(texts.join('\\n\\n'), MAX_PREVIEW_PARAGRAPHS)\n if (paragraphs.length === 0) return fallback\n return truncatePreview(paragraphs.join('\\n'), MAX_PREVIEW_CHARS)\n}\n\n/**\n * Derive the tick directory from the snapshot's ordered node list.\n * @param nodes - conversation snapshot nodes in seq order.\n * @param running - whether the session currently has a running turn.\n * @param labels - localized preview fallbacks.\n * @returns one item per human input, in order.\n */\nexport function buildTimelineItems(\n nodes: readonly TimelineNodeLike[],\n running: boolean,\n labels: TimelineLabels,\n): TimelineItem[] {\n interface Draft {\n key: string\n seq: number\n time: number\n userTexts: string[]\n hasUserText: boolean\n assistantTexts: string[]\n }\n const drafts: Draft[] = []\n for (const node of nodes) {\n if (HUMAN_KINDS.has(node.kind)) {\n const text = blocksText(node.content)\n drafts.push({\n key: `n${node.seq}`,\n seq: node.seq,\n time: typeof node.time === 'number' ? node.time : 0,\n userTexts: text === '' ? [] : [text],\n hasUserText: text !== '',\n assistantTexts: [],\n })\n continue\n }\n if (node.kind !== 'assistant' || drafts.length === 0) continue\n const text = blocksText(node.blocks)\n if (text !== '') drafts[drafts.length - 1].assistantTexts.push(text)\n }\n const last = drafts.length - 1\n return drafts.map((draft, index) => {\n const isLive = running && index === last\n const assistantTexts = draft.assistantTexts\n const assistantKind: TimelineItem['assistantKind'] = assistantTexts.length > 0\n ? 'text'\n : isLive\n ? 'running'\n : 'empty'\n return {\n key: draft.key,\n seq: draft.seq,\n time: draft.time,\n userPreview: buildPreview(draft.userTexts, labels.userFallback),\n userFallback: !draft.hasUserText,\n assistantPreview: buildPreview(assistantTexts, isLive ? labels.assistantRunning : labels.assistantEmpty),\n assistantKind,\n running: isLive,\n }\n })\n}\n","/**\n * Chat DOM probe — the only module allowed to know DSH's internal chat DOM.\n *\n * Contract measured on dsh 0.1.1-rc.2 (see .scratch/chat-timeline/issues/03):\n * the scroll container carries `data-conversation-scroll`; each flow row is a\n * `[data-chat-flow-kind]` element whose `user` kind marks a human turn, in the\n * same seq order as the snapshot's user nodes. Every access is defensive:\n * when detection fails the probe degrades to a no-op rail (no jump, no\n * highlight) instead of throwing.\n */\n\n/** Read-only view of the chat DOM the rail interacts with. */\nexport interface ChatDomProbe {\n /** The chat scroll container, or null when not found. */\n getContainer(): HTMLElement | null\n /** Human-turn row elements in document order (user kind only). */\n getUserRows(): HTMLElement[]\n /** Index of the human turn nearest the viewport top, or -1. */\n activeIndex(): number\n /** Scroll the given human turn into view; false when unavailable. */\n jumpTo(index: number, behavior: ScrollBehavior): boolean\n}\n\n/**\n * Find the chat scroll container with a candidate chain.\n * 1. the explicit stable hook `[data-conversation-scroll]`;\n * 2. the closest scrollable ancestor of the flow column `[data-chat-flow]`;\n * 3. the largest scrollable element that contains human rows.\n */\nexport function findChatContainer(doc: Document): HTMLElement | null {\n const direct = doc.querySelector<HTMLElement>('[data-conversation-scroll]')\n if (direct !== null) return direct\n const flow = doc.querySelector<HTMLElement>('[data-chat-flow]')\n const ancestor = flow !== null ? scrollableAncestor(flow) : null\n if (ancestor !== null) return ancestor\n let best: HTMLElement | null = null\n let bestHeight = 0\n for (const element of Array.from(doc.querySelectorAll<HTMLElement>('*'))) {\n if (!isScrollable(element)) continue\n if (element.querySelector('[data-chat-flow-kind=\"user\"]') === null) continue\n const height = element.clientHeight\n if (height > bestHeight) {\n best = element\n bestHeight = height\n }\n }\n return best\n}\n\nfunction isScrollable(element: HTMLElement): boolean {\n const style = element.ownerDocument.defaultView?.getComputedStyle(element)\n if (style === undefined) return false\n if (!/(auto|scroll)/.test(style.overflowY)) return false\n return element.scrollHeight > element.clientHeight + 40 && element.clientHeight > 200\n}\n\nfunction scrollableAncestor(element: HTMLElement): HTMLElement | null {\n let node: HTMLElement | null = element\n while (node !== null) {\n if (isScrollable(node)) return node\n node = node.parentElement\n }\n return null\n}\n\n/** Human-turn rows inside the container (falling back to a document-wide query). */\nexport function findUserRows(container: HTMLElement | null): HTMLElement[] {\n const scope: ParentNode = container ?? document\n return Array.from(scope.querySelectorAll<HTMLElement>('[data-chat-flow-kind=\"user\"]'))\n}\n\n/**\n * The active index: the last human row whose top edge sits above the\n * container's reading line (40px below the top edge), matching the ZCode\n * \"unit at viewport top\" rule; clamps to the last row at scroll bottom.\n */\nexport function computeActiveIndex(container: HTMLElement, rows: readonly HTMLElement[]): number {\n if (rows.length === 0) return -1\n const top = container.getBoundingClientRect().top + 40\n let active = 0\n for (let index = 0; index < rows.length; index += 1) {\n if (rows[index].getBoundingClientRect().top <= top) active = index\n else break\n }\n const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight\n if (distanceToBottom <= 4) return rows.length - 1\n return active\n}\n\n/**\n * Scroll the given human turn into view. Tries a smooth scroll first; DSH's\n * windowed conversation list programs scrollTop on scroll events, which can\n * cancel a smooth animation on the first frame — when nothing has moved after\n * a short grace period the jump falls back to an instant scroll.\n * @returns false when the container or row is unavailable.\n */\nfunction jumpTo(index: number, behavior: ScrollBehavior): boolean {\n const container = findChatContainer(document)\n if (container === null) return false\n const row = findUserRows(container)[index]\n if (row === undefined) return false\n const delta = row.getBoundingClientRect().top - container.getBoundingClientRect().top - 12\n const target = container.scrollTop + delta\n if (behavior === 'auto') {\n container.scrollTo({ top: target, behavior: 'auto' })\n return true\n }\n const startedAt = container.scrollTop\n container.scrollTo({ top: target, behavior: 'smooth' })\n window.setTimeout(() => {\n // Still within a few pixels of the start after the grace period: the\n // smooth animation was cancelled — land instantly instead.\n if (Math.abs(container.scrollTop - startedAt) < 4) {\n container.scrollTo({ top: target, behavior: 'auto' })\n }\n }, 700)\n return true\n}\n\n/** Create the probe against a document (injectable for tests). */\nexport function probeChatDom(doc: Document): ChatDomProbe {\n return {\n getContainer: () => findChatContainer(doc),\n getUserRows: () => findUserRows(findChatContainer(doc)),\n activeIndex: () => {\n const container = findChatContainer(doc)\n if (container === null) return -1\n return computeActiveIndex(container, findUserRows(container))\n },\n jumpTo: (index: number, behavior: ScrollBehavior) => jumpTo(index, behavior),\n }\n}\n","/**\n * Stylesheet for the chat-timeline rail, injected once as a\n * `<style data-plugin-css=\"chat-timeline\">` tag. All classes carry the\n * `dsh-tl-` prefix; colors use the CSS system colors Canvas/CanvasText so the\n * rail follows the app's color-scheme without reaching into DSH theme tokens.\n * Visual constants replicate the ZCode TurnNavigator (spec section 2.2).\n */\nexport const TIMELINE_STYLES = `\n.dsh-tl-nav {\n position: fixed;\n z-index: 10;\n width: 48px;\n pointer-events: none;\n opacity: 1;\n transition: opacity 150ms ease-out;\n}\n.dsh-tl-nav[data-visible=\"false\"] {\n opacity: 0;\n visibility: hidden;\n}\n.dsh-tl-scroll {\n position: absolute;\n left: 12px;\n top: 50%;\n transform: translateY(-50%);\n width: 36px;\n max-height: calc(100% - 96px);\n overflow-x: hidden;\n overflow-y: auto;\n padding-block: 4px;\n pointer-events: auto;\n scrollbar-width: none;\n color: CanvasText;\n}\n.dsh-tl-scroll::-webkit-scrollbar {\n display: none;\n}\n.dsh-tl-track {\n position: relative;\n width: 36px;\n}\n.dsh-tl-slot {\n position: absolute;\n left: 0;\n top: 0;\n height: 10px;\n width: 36px;\n padding: 0;\n border: 0;\n background: transparent;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n border-radius: 2px;\n cursor: pointer;\n}\n.dsh-tl-slot:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n}\n.dsh-tl-tick {\n display: block;\n height: 2px;\n width: 12px;\n border-radius: 999px;\n background: currentColor;\n transform-origin: left center;\n transition: height 150ms ease-out, opacity 150ms ease-out, transform 150ms ease-out, background-color 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .dsh-tl-nav,\n .dsh-tl-tick {\n transition: none;\n }\n}\n.dsh-tl-tip {\n position: fixed;\n z-index: 60;\n width: 320px;\n max-width: calc(100vw - 2rem);\n padding: 12px;\n border-radius: 10px;\n background: Canvas;\n color: CanvasText;\n border: 1px solid color-mix(in srgb, CanvasText 15%, transparent);\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);\n pointer-events: none;\n}\n.dsh-tl-tip-user {\n margin: 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n}\n.dsh-tl-tip-assistant {\n margin: 8px 0 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n opacity: 0.8;\n}\n.dsh-tl-tip-assistant[data-kind=\"running\"],\n.dsh-tl-tip-assistant[data-kind=\"empty\"] {\n opacity: 0.55;\n}\n`\n\n/** Inject the stylesheet once per document. */\nexport function ensureStyles(doc: Document): void {\n const tagId = 'chat-timeline'\n if (doc.querySelector('style[data-plugin-css=\"chat-timeline\"]') !== null) return\n const tag = doc.createElement('style')\n tag.dataset.plugin = '@lament_z/dsh-client-ui-chat-timeline'\n tag.dataset.pluginCss = tagId\n tag.textContent = TIMELINE_STYLES\n doc.head.appendChild(tag)\n}\n","/**\n * TimelineRail — the shell.overlay entry: a left-edge tick rail over the\n * conversation, one tick per human question. Visual and interaction constants\n * replicate the ZCode TurnNavigator (see .scratch/chat-timeline/spec.md):\n * hover ripple scaleX 2.6/1.7/1.25 with opacity 1/.86/.72/.58 over 150ms,\n * 320px preview cards after a 120ms delay, >=2 questions to render, >=864px\n * container width, prefers-reduced-motion fallback, full aria labelling.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport { buildTimelineItems, type TimelineItem, type TimelineLabels } from './directory.ts'\nimport type { TimelineKey } from './locales.ts'\nimport { probeChatDom, type ChatDomProbe } from './dom.ts'\nimport { ensureStyles } from './styles.ts'\nimport type { TimelineSource } from './source.ts'\n\n/** Component props: the locale seat plus the injected session source. */\nexport type TimelineRailProps = PropsLocale<'chat-timeline'> & {\n /** The current-session snapshot source (built from ctx.sessions). */\n source: TimelineSource\n}\n\n/** Slot height per tick (10px pitch, ZCode parity). */\nconst ITEM_PITCH = 10\n/** Container width below which the rail hides. ZCode uses 864px, but DSH's\n * three-pane layout leaves the conversation narrower — the rail overlays the\n * left edge without taking layout space, so only truly cramped panes hide. */\nconst MIN_CONTAINER_WIDTH = 560\n/** Preview-card hover delays (ms). */\nconst TIP_OPEN_DELAY = 120\nconst TIP_CLOSE_DELAY = 80\n\ninterface Rect {\n left: number\n top: number\n height: number\n width: number\n}\n\n/** Ripple visual per distance from the hovered tick (ZCode _5e parity).\n * The ripple is interaction-only: at rest every tick is idle (scaleX 1),\n * and the current position is signalled by color/opacity, not length. */\nfunction ripple(distance: number): { opacity: number; scaleX: number; tone: 'peak' | 'near' | 'mid' | 'idle' } {\n if (distance === 0) return { opacity: 1, scaleX: 2.6, tone: 'peak' }\n if (distance === 1) return { opacity: 0.86, scaleX: 1.7, tone: 'near' }\n if (distance === 2) return { opacity: 0.72, scaleX: 1.25, tone: 'mid' }\n return { opacity: 0.58, scaleX: 1, tone: 'idle' }\n}\n\n/** Tick colors: foreground for the hovered peak and the current turn at rest,\n * the subtlest text tone otherwise (ZCode bg-foreground / -subtlest parity\n * through system colors). */\nconst TICK_COLOR_FOREGROUND = 'CanvasText'\nconst TICK_COLOR_SUBTLE = 'color-mix(in srgb, CanvasText 42%, transparent)'\n\nfunction useReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false)\n useEffect(() => {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return\n const query = window.matchMedia('(prefers-reduced-motion: reduce)')\n const update = () => setReduced(query.matches)\n update()\n query.addEventListener('change', update)\n return () => query.removeEventListener('change', update)\n }, [])\n return reduced\n}\n\n/**\n * Render the question navigator rail.\n * @param props - composed slot props.\n * @returns the rail, or null when it should not render.\n */\nexport function TimelineRail({ source, t }: TimelineRailProps) {\n const state = useSyncExternalStore(source.subscribe, source.getSnapshot)\n const reducedMotion = useReducedMotion()\n const probe = useMemo<ChatDomProbe | null>(() => (typeof document === 'undefined' ? null : probeChatDom(document)), [])\n const [containerVersion, setContainerVersion] = useState(0)\n const [rect, setRect] = useState<Rect | null>(null)\n const [activeIndex, setActiveIndex] = useState(-1)\n const [hoverIndex, setHoverIndex] = useState<number | undefined>(undefined)\n const [tipIndex, setTipIndex] = useState<number | undefined>(undefined)\n const timers = useRef<{ open: number | undefined; close: number | undefined }>({ open: undefined, close: undefined })\n const frame = useRef(0)\n\n const labels: TimelineLabels = useMemo(\n () => ({\n userFallback: t('preview.userFallback'),\n assistantEmpty: t('preview.assistantEmpty'),\n assistantRunning: t('preview.assistantRunning'),\n }),\n [t],\n )\n\n const snapshot = state.snapshot\n const items = useMemo<TimelineItem[]>(\n () => (snapshot === null ? [] : buildTimelineItems(snapshot.nodes as never[], snapshot.running, labels)),\n [snapshot, labels],\n )\n\n useEffect(() => {\n if (typeof document === 'undefined') return\n ensureStyles(document)\n }, [])\n\n // Find the chat container: re-probe when the session changes and briefly\n // afterwards (the conversation mounts a beat later than the selection).\n useEffect(() => {\n if (probe === null) return\n let attempts = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n const tick = () => {\n const container = probe.getContainer()\n if (container !== null || attempts >= 10) {\n setContainerVersion((version) => version + 1)\n return\n }\n attempts += 1\n timer = setTimeout(tick, 300)\n }\n tick()\n return () => {\n if (timer !== undefined) clearTimeout(timer)\n }\n }, [probe, state.sessionId])\n\n // Attach scroll/size watchers to the container once found.\n useEffect(() => {\n if (probe === null || containerVersion === 0) return\n const container = probe.getContainer()\n if (container === null) {\n setRect(null)\n return\n }\n const sync = () => {\n if (frame.current !== 0) return\n frame.current = requestAnimationFrame(() => {\n frame.current = 0\n const box = container.getBoundingClientRect()\n setRect({ left: box.left, top: box.top, height: box.height, width: box.width })\n setActiveIndex(probe.activeIndex())\n })\n }\n sync()\n container.addEventListener('scroll', sync, { passive: true })\n const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(sync)\n observer?.observe(container)\n window.addEventListener('resize', sync)\n return () => {\n container.removeEventListener('scroll', sync)\n observer?.disconnect()\n window.removeEventListener('resize', sync)\n if (frame.current !== 0) cancelAnimationFrame(frame.current)\n frame.current = 0\n }\n }, [probe, containerVersion])\n\n const visible = snapshot !== null && items.length >= 2 && rect !== null && rect.width >= MIN_CONTAINER_WIDTH\n // The ripple follows the pointer only (ZCode v5e parity): at rest every tick\n // is equal length and the current turn stands out by color/opacity alone.\n const focusIndex = hoverIndex\n\n const scheduleTip = useCallback((index: number | undefined) => {\n if (timers.current.open !== undefined) window.clearTimeout(timers.current.open)\n if (timers.current.close !== undefined) window.clearTimeout(timers.current.close)\n if (index === undefined) {\n timers.current.close = window.setTimeout(() => setTipIndex(undefined), TIP_CLOSE_DELAY)\n return\n }\n timers.current.open = window.setTimeout(() => setTipIndex(index), TIP_OPEN_DELAY)\n }, [])\n\n useEffect(() => () => {\n if (timers.current.open !== undefined) window.clearTimeout(timers.current.open)\n if (timers.current.close !== undefined) window.clearTimeout(timers.current.close)\n }, [])\n\n const jump = useCallback((index: number) => {\n probe?.jumpTo(index, reducedMotion ? 'auto' : 'smooth')\n }, [probe, reducedMotion])\n\n if (!visible || probe === null || rect === null) return null\n\n const trackHeight = items.length * ITEM_PITCH\n const tip = tipIndex !== undefined ? items[tipIndex] : undefined\n\n return (\n <>\n <nav\n aria-label={t('nav.label')}\n className=\"dsh-tl-nav\"\n data-visible={visible ? 'true' : 'false'}\n data-testid=\"dsh-chat-timeline\"\n data-item-count={items.length}\n style={{ left: rect.left, top: rect.top, height: rect.height }}\n >\n <div\n className=\"dsh-tl-scroll\"\n onPointerLeave={() => {\n setHoverIndex(undefined)\n scheduleTip(undefined)\n }}\n >\n <div className=\"dsh-tl-track\" style={{ height: `${trackHeight}px` }}>\n {items.map((item, index) => {\n const isActive = index === activeIndex\n const distance = focusIndex === undefined ? 3 : Math.abs(index - focusIndex)\n const look = ripple(distance)\n const activeAtRest = focusIndex === undefined && isActive\n const foreground = look.tone === 'peak' || activeAtRest\n const opacity = activeAtRest\n ? 0.9\n : item.running\n ? Math.max(look.opacity, 0.72)\n : look.opacity\n return (\n <div\n key={item.key}\n className=\"dsh-tl-slot\"\n style={{ transform: `translateY(${index * ITEM_PITCH}px)` }}\n onMouseEnter={() => {\n setHoverIndex(index)\n scheduleTip(index)\n }}\n onFocus={() => {\n setHoverIndex(index)\n scheduleTip(index)\n }}\n onBlur={() => {\n setHoverIndex(undefined)\n scheduleTip(undefined)\n }}\n >\n <button\n type=\"button\"\n aria-current={isActive ? 'location' : undefined}\n aria-label={t('nav.jumpToQuery', { index: String(index + 1) })}\n aria-posinset={index + 1}\n aria-setsize={items.length}\n data-testid=\"dsh-chat-timeline-item\"\n data-item-index={index}\n data-active={isActive ? 'true' : 'false'}\n data-running={item.running ? 'true' : 'false'}\n onClick={() => jump(index)}\n className=\"dsh-tl-slot-inner\"\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-start',\n width: '100%',\n height: '100%',\n padding: 0,\n border: 0,\n background: 'transparent',\n cursor: 'pointer',\n }}\n >\n <span\n className=\"dsh-tl-tick\"\n style={{\n opacity,\n transform: `scaleX(${look.scaleX})`,\n backgroundColor: foreground ? TICK_COLOR_FOREGROUND : TICK_COLOR_SUBTLE,\n }}\n />\n </button>\n </div>\n )\n })}\n </div>\n </div>\n </nav>\n {tip !== undefined && tipIndex !== undefined\n ? createPortal(\n <TipCard item={tip} index={tipIndex} rect={rect} t={t} />,\n document.body,\n )\n : null}\n </>\n )\n}\n\nfunction TipCard({ item, index, rect, t }: {\n item: TimelineItem\n index: number\n rect: Rect\n t: (key: TimelineKey, params?: Record<string, string | number>) => string\n}) {\n const slot = document.querySelector<HTMLElement>('[data-testid=\"dsh-chat-timeline-item\"][data-item-index=\"' + String(index) + '\"]')\n const box = slot?.getBoundingClientRect()\n const left = box === undefined ? rect.left + 56 : Math.min(box.right + 8, window.innerWidth - 336)\n const top = box === undefined ? rect.top + 100 : Math.max(8, Math.min(box.top - 4, window.innerHeight - 160))\n return (\n <div\n className=\"dsh-tl-tip\"\n data-testid=\"dsh-chat-timeline-tip\"\n style={{ left, top }}\n role=\"tooltip\"\n >\n <p className=\"dsh-tl-tip-user\">{item.userPreview}</p>\n <p className=\"dsh-tl-tip-assistant\" data-kind={item.assistantKind}>{item.assistantPreview}</p>\n </div>\n )\n}\n","/**\n * Timeline source: follows the current session through the cordis services\n * face (ctx.sessions) and exposes a useSyncExternalStore-compatible read\n * face over its ConversationSnapshot. All dsh-client-runtime usage here is\n * type-only; at runtime everything is reached through the ctx services, so\n * the client bundle stays free of cross-package value imports.\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\n\n/** The slice of ConversationSnapshot the timeline consumes. */\nexport interface TimelineSnapshot {\n readonly sessionId: string\n /** Ordered conversation nodes (the snapshot's legacy compatibility field). */\n readonly nodes: readonly unknown[]\n /** Whether the session currently has a running turn. */\n readonly running: boolean\n}\n\n/** State handed to React: null snapshot means \"no readable session right now\". */\nexport interface TimelineState {\n readonly sessionId: string | undefined\n readonly snapshot: TimelineSnapshot | null\n}\n\n/** useSyncExternalStore-compatible source face. */\nexport interface TimelineSource {\n subscribe(listener: () => void): () => void\n getSnapshot(): TimelineState\n}\n\ninterface ObservableLike<T> {\n getSnapshot(): T\n subscribe(listener: () => void): () => void\n}\n\n/** Structural face of ctx.sessions this module needs (kept narrow for tests). */\nexport interface SessionsLike {\n readonly list: ObservableLike<{ current?: string }>\n binding(id: string): { session: ObservableLike<{ nodes?: readonly unknown[]; running?: boolean }> } | undefined\n}\n\nconst EMPTY_STATE: TimelineState = { sessionId: undefined, snapshot: null }\n\n/**\n * Create the source. Subscribes to the sessions list, rebinds to the current\n * session's snapshot feed on selection change, and republishes a stable state\n * object whenever the underlying snapshot changes.\n * @param sessions - the sessions service face (pass ctx.sessions).\n * @returns the source face.\n */\nexport function createTimelineSource(sessions: SessionsLike): TimelineSource {\n let state: TimelineState = EMPTY_STATE\n const listeners = new Set<() => void>()\n let unbindSession: (() => void) | null = null\n let boundSessionId: string | undefined\n let retryTimer: ReturnType<typeof setTimeout> | undefined\n let retryAttempts = 0\n\n const emit = () => {\n for (const listener of listeners) listener()\n }\n\n const rebind = () => {\n if (retryTimer !== undefined) {\n clearTimeout(retryTimer)\n retryTimer = undefined\n }\n let current: string | undefined\n try {\n current = sessions.list.getSnapshot()?.current\n } catch {\n current = undefined\n }\n if (current === boundSessionId && retryAttempts === 0) return\n boundSessionId = current\n unbindSession?.()\n unbindSession = null\n if (current === undefined) {\n state = EMPTY_STATE\n emit()\n return\n }\n let session: ObservableLike<{ nodes?: readonly unknown[]; running?: boolean }> | undefined\n try {\n session = sessions.binding(current)?.session\n } catch {\n session = undefined\n }\n if (!session) {\n // Listed but not yet scoped (cold boot): keep an empty state and retry\n // with backoff so the rail appears once the binding lands.\n state = { sessionId: current, snapshot: null }\n if (retryAttempts < 10) {\n retryAttempts += 1\n retryTimer = setTimeout(rebind, 500 * retryAttempts)\n }\n emit()\n return\n }\n retryAttempts = 0\n const push = () => {\n try {\n const snap = session.getSnapshot()\n state = {\n sessionId: current,\n snapshot: {\n sessionId: current,\n nodes: Array.isArray(snap?.nodes) ? snap.nodes : [],\n running: Boolean(snap?.running),\n },\n }\n } catch {\n state = { sessionId: current, snapshot: null }\n }\n emit()\n }\n push()\n try {\n session.subscribe(push)\n unbindSession = () => {\n // Observable faces expose no unsubscribe disposer contract here; the\n // subscription rides the plugin fiber and is torn down with it.\n }\n } catch {\n unbindSession = null\n }\n }\n\n return {\n subscribe(listener: () => void): () => void {\n const first = listeners.size === 0\n listeners.add(listener)\n if (first) {\n try {\n sessions.list.subscribe(rebind)\n } catch {\n // Service unavailable: the rail stays hidden, nothing throws.\n }\n rebind()\n }\n return () => {\n listeners.delete(listener)\n }\n },\n getSnapshot(): TimelineState {\n return state\n },\n }\n}\n\n/** Build the source from the plugin client context. */\nexport function timelineSourceFromContext(ctx: ClientContext): TimelineSource {\n return createTimelineSource(ctx.sessions as unknown as SessionsLike)\n}\n","/**\n * Locale dictionaries for the chat-timeline plugin. `zh` is the key-set source\n * of truth; `en` keeps a full key-for-key mirror. Registered through\n * ctx.locale.register(NS, { zh, en }).\n */\n\n/** Simplified Chinese dictionary (key-set source of truth). */\nexport const zh = {\n 'nav.label': '对话问题导航',\n 'nav.jumpToQuery': '跳转到第 {index} 条问题',\n 'preview.userFallback': '用户输入',\n 'preview.assistantEmpty': '暂无助手正文',\n 'preview.assistantRunning': '助手仍在工作',\n}\n\n/** The chat-timeline namespace key union. */\nexport type TimelineKey = keyof typeof zh\n\n/** English dictionary, key-for-key complete against zh. */\nexport const en: Record<TimelineKey, string> = {\n 'nav.label': 'Conversation question navigator',\n 'nav.jumpToQuery': 'Jump to question {index}',\n 'preview.userFallback': 'User input',\n 'preview.assistantEmpty': 'No assistant text yet',\n 'preview.assistantRunning': 'Assistant is still working',\n}\n","/**\n * Chat-timeline plugin — browser half. Registers the `chat-timeline` locale\n * dictionaries and a `shell.overlay` entry that renders the question\n * navigator rail beside the conversation. Export discipline: the /client\n * surface carries only what cordis loading needs plus types.\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale) and its\n// LocaleNamespaceMap merge table.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ui-layout SlotMap merge (the 'shell.overlay' hole).\nimport type {} from '@deepseek-ai/dsh-client-ui-layout/client'\nimport { TimelineRail } from './rail.tsx'\nimport { timelineSourceFromContext } from './source.ts'\nimport { en, zh, type TimelineKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Chat-timeline surface copy. */\n 'chat-timeline': TimelineKey\n }\n}\n\n/** Dictionary namespace owned by this plugin. */\nconst NS = 'chat-timeline'\n\n/** Unique occupant id inside the shared shell.overlay list slot. */\nconst ENTRY_ID = 'chat-timeline'\n\n/** Services required by this plugin. */\nexport const inject = ['slots', 'locale', 'sessions']\n\n/**\n * Register the timeline surface.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => {\n try {\n return ctx.locale.register(NS, { zh, en })\n } catch {\n return () => {}\n }\n }, 'chat-timeline: dictionaries')\n\n // The rail floats over the conversation via the frame-wide additive seat.\n // Registration is declaration-aware via slots.inject.\n ctx.slots.inject('shell.overlay', () => {\n try {\n return ctx.slots.register({\n name: 'shell.overlay',\n id: ENTRY_ID,\n locale: NS,\n inject: () => ({ source: timelineSourceFromContext(ctx) }),\n }, TimelineRail)\n } catch {\n return () => {}\n }\n })\n}\n\nexport type { TimelineRailProps } from './rail.tsx'\nexport type { TimelineKey } from './locales.ts'\nexport type { TimelineSource, TimelineSnapshot, TimelineState } from './source.ts'\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client/dom.ts","../src/client/styles.ts","../src/client/rail.tsx","../src/client/source.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Chat DOM probe — the only module allowed to know DSH's internal chat DOM.\n *\n * Contract replicated 1:1 from the native chat view (dsh-client-ui-chat\n * 0.1.5-rc.2, ChatView):\n * - every rendered row carries `data-chat-turn=\"<turn>\"` (plus\n * `data-chat-anchor-key` / `data-chat-flow-key` with the node key, which is\n * NOT seq-prefixed — never parse seqs out of flow keys);\n * - the scrollport is `row.closest('[data-conversation-scroll]')`;\n * - landing on a turn is an instant `el.scrollTop += flowTop(row, el) - 24`;\n * - a turn outside the loaded event window is paged in through the official\n * `session.loadThrough(turn/start seq)` verb, then landed once its row\n * renders (the native pendingJump/settle loop, plugin-side as a poll);\n * - the active turn reads the line `top + min(96, height * 0.2)` via\n * `elementsFromPoint` hit-testing with a row-scan fallback, and the last\n * turn wins within 25px of the scroll bottom.\n * Every access is defensive: when detection fails the probe degrades to a\n * no-op rail (no jump, no highlight) instead of throwing.\n */\n\n/** Read-only view of the chat DOM the rail interacts with. */\nexport interface ChatDomProbe {\n /** The chat scroll container, or null when not found. */\n getContainer(): HTMLElement | null\n /** Index of the tick nearest the reading line, or -1. */\n activeIndex(turns: readonly (number | undefined)[]): number\n}\n\n/**\n * Find the chat scroll container with a candidate chain.\n * 1. the explicit stable hook `[data-conversation-scroll]`;\n * 2. the closest scrollable ancestor of the flow column `[data-chat-flow]`;\n * 3. the largest scrollable element that contains keyed flow items.\n */\nexport function findChatContainer(doc: Document): HTMLElement | null {\n const direct = doc.querySelector<HTMLElement>('[data-conversation-scroll]')\n if (direct !== null) return direct\n const flow = doc.querySelector<HTMLElement>('[data-chat-flow]')\n const ancestor = flow !== null ? scrollableAncestor(flow) : null\n if (ancestor !== null) return ancestor\n let best: HTMLElement | null = null\n let bestHeight = 0\n for (const element of Array.from(doc.querySelectorAll<HTMLElement>('*'))) {\n if (!isScrollable(element)) continue\n if (element.querySelector('[data-chat-turn]') === null) continue\n const height = element.clientHeight\n if (height > bestHeight) {\n best = element\n bestHeight = height\n }\n }\n return best\n}\n\nfunction isScrollable(element: HTMLElement): boolean {\n const style = element.ownerDocument.defaultView?.getComputedStyle(element)\n if (style === undefined) return false\n if (!/(auto|scroll)/.test(style.overflowY)) return false\n return element.scrollHeight > element.clientHeight + 40 && element.clientHeight > 200\n}\n\nfunction scrollableAncestor(element: HTMLElement): HTMLElement | null {\n let node: HTMLElement | null = element\n while (node !== null) {\n if (isScrollable(node)) return node\n node = node.parentElement\n }\n return null\n}\n\n/**\n * The rendered row of `turn` (native anchor attribute `data-chat-turn`),\n * skipping hidden rows like the native `anchorElement`. Scoped to the chat\n * container when it is already located (native scopes to its own list), so a\n * stray second mount cannot hijack the anchor. Null when the turn's events\n * are not in the loaded window yet.\n */\nexport function findTurnRow(doc: Document, turn: number | undefined): HTMLElement | null {\n if (turn === undefined || !Number.isSafeInteger(turn) || turn < 0) return null\n const scope: ParentNode = findChatContainer(doc) ?? doc\n return scope.querySelector<HTMLElement>(`[data-chat-turn=\"${turn}\"]:not([hidden])`)\n}\n\n/** Native `scrollerOf`: the conversation scrollport owning the row. */\nfunction scrollerOfRow(doc: Document, row: HTMLElement): HTMLElement | null {\n return row.closest<HTMLElement>('[data-conversation-scroll]') ?? findChatContainer(doc)\n}\n\n/** Row position in scrollport coordinates (native `flowTop`). */\nfunction flowTop(row: HTMLElement, scroller: HTMLElement): number {\n return row.getBoundingClientRect().top - scroller.getBoundingClientRect().top\n}\n\n/**\n * Native `landOnRow`: instant scroll so the row's top sits 24px below the\n * scrollport top. Returns false when the scrollport cannot be resolved.\n */\nexport function landOnRow(doc: Document, row: HTMLElement): boolean {\n const el = scrollerOfRow(doc, row)\n if (el === null) return false\n el.scrollTop += flowTop(row, el) - 24\n return true\n}\n\n/**\n * Native `turnAtLine`: the turn owning the row at a scrollport line.\n * Hit-tests the line first (elementsFromPoint at the container's horizontal\n * center) and falls back to one row scan when layout cannot answer (jsdom,\n * pre-paint). Returns null when no loaded row covers the line.\n */\nexport function turnAtLine(doc: Document, container: HTMLElement, line: number): number | null {\n const content = container.getBoundingClientRect()\n if (typeof doc.elementsFromPoint === 'function' && content.width > 0) {\n for (const element of doc.elementsFromPoint(content.left + content.width / 2, line)) {\n const row = element instanceof HTMLElement ? element.closest<HTMLElement>('[data-chat-turn]') : null\n const turn = Number(row?.dataset.chatTurn)\n if (row !== null && container.contains(row) && Number.isSafeInteger(turn)) return turn\n }\n }\n let found: number | null = null\n for (const row of Array.from(container.querySelectorAll<HTMLElement>('[data-chat-turn]'))) {\n if (row.getBoundingClientRect().top > line) break\n const turn = Number(row.dataset.chatTurn)\n if (Number.isSafeInteger(turn)) found = turn\n }\n return found\n}\n\n/**\n * The active index (native `syncActiveTurn`): within 25px of the scroll\n * bottom the last tick wins; otherwise the reading line sits at\n * `top + min(96, height * 0.2)` and the active tick is the last one whose\n * turn does not exceed the turn read at that line. When the line covers no\n * loaded row, earlier ticks outside the window count as above the viewport\n * (the window head's predecessor is active, matching 0.1.1 parity).\n */\nexport function computeActiveIndex(\n container: HTMLElement,\n doc: Document,\n turns: readonly (number | undefined)[],\n): number {\n if (turns.length === 0) return -1\n if (container.scrollHeight - container.scrollTop - container.clientHeight <= 25) {\n return turns.length - 1\n }\n const firstDefined = turns.findIndex((turn) => turn !== undefined)\n if (firstDefined === -1) return -1\n const reading = container.getBoundingClientRect().top + Math.min(96, container.clientHeight * 0.2)\n const readTurn = turnAtLine(doc, container, reading)\n let active = -1\n if (readTurn !== null) {\n for (let index = 0; index < turns.length; index += 1) {\n const turn = turns[index]\n if (turn === undefined || turn > readTurn) continue\n active = index\n }\n }\n if (active !== -1) return active\n return firstDefined > 0 ? firstDefined - 1 : 0\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n/** Landing poll budget after `loadThrough` (native settles via its effects; 5s here). */\nconst LAND_POLL_ATTEMPTS = 20\nconst LAND_POLL_INTERVAL_MS = 250\n\n/**\n * 跳到第 index 个刻度的完整落点流程(原生 navigateToTurn 的插件侧等价物):\n * 1. 按 `data-chat-turn` 找行并原生落位(瞬时、行顶距视口顶 24px);\n * 2. 行未挂载时先 `loadThrough(turn/start 的 seq)` 把事件窗口翻页到位;\n * 3. 轮询等行渲染后落位。任何一步成功即返回 true。\n */\nexport async function jumpToTurn(\n doc: Document,\n index: number,\n turns: readonly (number | undefined)[],\n seqs: readonly number[],\n loadThrough: ((seq: number) => Promise<void>) | undefined,\n): Promise<boolean> {\n const turn = turns[index]\n if (turn === undefined) return false\n if (landOnRowByTurn(doc, turn)) return true\n const seq = seqs[index]\n if (seq !== undefined && loadThrough !== undefined) {\n try {\n await loadThrough(seq)\n } catch {\n // 翻页失败仍然轮询(窗口可能已部分就位)。\n }\n }\n for (let attempt = 0; attempt < LAND_POLL_ATTEMPTS; attempt += 1) {\n await sleep(LAND_POLL_INTERVAL_MS)\n if (landOnRowByTurn(doc, turn)) return true\n }\n return false\n}\n\nfunction landOnRowByTurn(doc: Document, turn: number): boolean {\n const row = findTurnRow(doc, turn)\n if (row === null) return false\n return landOnRow(doc, row)\n}\n\n/** Create the probe against a document (injectable for tests). */\nexport function probeChatDom(doc: Document): ChatDomProbe {\n return {\n getContainer: () => findChatContainer(doc),\n activeIndex: (turns) => {\n const container = findChatContainer(doc)\n if (container === null) return -1\n return computeActiveIndex(container, doc, turns)\n },\n }\n}\n","/**\n * Stylesheet for the chat-timeline rail, injected once as a\n * `<style data-plugin-css=\"chat-timeline\">` tag. All classes carry the\n * `dsh-tl-` prefix; colors use the CSS system colors Canvas/CanvasText so the\n * rail follows the app's color-scheme without reaching into DSH theme tokens.\n * Visual constants replicate the ZCode TurnNavigator (spec section 2.2).\n */\nexport const TIMELINE_STYLES = `\n.dsh-tl-nav {\n position: fixed;\n z-index: 10;\n width: 48px;\n /* 水平:会话列左沿,由 rail 写入 --dsh-tl-left。这是唯一保留的测量值——\n 列是 grid 轨道,右侧栏打开时会被挤压,不贴窗口右沿,无法由窗口宽度推导。 */\n left: var(--dsh-tl-left, 0px);\n /* 垂直:原生 TurnNavigator 的 band 模型(eGxaPq_frame 的 --turn-rail-band)。\n 会话可视带 = 视口高 − 输入框高;其中点在视口坐标下为\n 100dvh − (会话可视高 + 输入框高) / 2\n 两个变量由宿主写在滚动容器上,本 seat 在滚动容器内可直接继承,故整个垂直\n 几何是纯 CSS:不做测量,也不依赖原生 nav 是否存在于 DOM。 */\n --dsh-tl-band: calc(var(--dsh-conversation-viewport-height, 100dvh) - var(--dsh-composer-height, 152px));\n top: calc(100dvh - (var(--dsh-conversation-viewport-height, 100dvh) + var(--dsh-composer-height, 152px)) / 2);\n transform: translateY(-50%);\n pointer-events: none;\n opacity: 1;\n transition: opacity 150ms ease-out;\n}\n.dsh-tl-nav[data-visible=\"false\"] {\n opacity: 0;\n visibility: hidden;\n}\n.dsh-tl-scroll {\n position: relative;\n left: 12px;\n width: 36px;\n /* 原生 frame 的高度上限:band − 64px,再夹到 420px。 */\n max-height: min(max(0px, calc(var(--dsh-tl-band) - 64px)), 420px);\n overflow-x: hidden;\n overflow-y: auto;\n padding-block: 4px;\n pointer-events: auto;\n scrollbar-width: none;\n color: CanvasText;\n}\n.dsh-tl-scroll::-webkit-scrollbar {\n display: none;\n}\n.dsh-tl-track {\n position: relative;\n width: 36px;\n}\n.dsh-tl-slot {\n position: absolute;\n left: 0;\n top: 0;\n height: 10px;\n width: 36px;\n padding: 0;\n border: 0;\n background: transparent;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n border-radius: 2px;\n cursor: pointer;\n}\n.dsh-tl-slot:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n}\n.dsh-tl-tick {\n display: block;\n height: 2px;\n width: 12px;\n border-radius: 999px;\n background: currentColor;\n transform-origin: left center;\n transition: height 150ms ease-out, opacity 150ms ease-out, transform 150ms ease-out, background-color 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .dsh-tl-nav,\n .dsh-tl-tick {\n transition: none;\n }\n}\n.dsh-tl-tip {\n position: fixed;\n z-index: 60;\n width: 320px;\n max-width: calc(100vw - 2rem);\n padding: 12px;\n border-radius: 10px;\n /* 原生 tooltip 配对(dsh-web-frontend _bubble_):灰底 + 静态白字,\n 主题切换由别名变量跟随;灰底与页面背景天然分层。 */\n background: var(--dsw-alias-tooltip-bg, #43454a);\n color: var(--dsw-static-neutral-bluish-00, #fff);\n border: none;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);\n pointer-events: none;\n}\n.dsh-tl-tip-user {\n margin: 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n}\n.dsh-tl-tip-assistant {\n margin: 8px 0 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-static-neutral-bluish-300, rgba(255, 255, 255, 0.8));\n}\n.dsh-tl-tip-assistant[data-kind=\"running\"],\n.dsh-tl-tip-assistant[data-kind=\"empty\"] {\n color: var(--dsw-static-neutral-bluish-400, rgba(255, 255, 255, 0.55));\n}\n/* 插件完全接管原生右侧 Turn 导航条:本样式表只随插件存在,插件卸载时\n removeStyles 移除标签,原生导航条立即恢复。aria-label 跟随宿主语言\n (英文 Turn navigation / 中文 轮次导航),两个都要盖住。 */\nnav[aria-label='Turn navigation'],\nnav[aria-label='轮次导航'] {\n display: none !important;\n}\n`\n\n/** Inject the stylesheet once per document. */\nexport function ensureStyles(doc: Document): void {\n const tagId = 'chat-timeline'\n if (doc.querySelector('style[data-plugin-css=\"chat-timeline\"]') !== null) return\n const tag = doc.createElement('style')\n tag.dataset.plugin = '@lament_z/dsh-client-ui-chat-timeline'\n tag.dataset.pluginCss = tagId\n tag.textContent = TIMELINE_STYLES\n doc.head.appendChild(tag)\n}\n\n/** Remove the stylesheet (plugin teardown): restores the native turn navigator. */\nexport function removeStyles(doc: Document): void {\n doc.querySelector('style[data-plugin-css=\"chat-timeline\"]')?.remove()\n}\n","/**\n * TimelineRail — the conversation.input.overlay entry: a left-edge tick rail\n * over the conversation, one tick per human question. Visual and interaction\n * constants replicate the ZCode TurnNavigator (see .scratch/chat-timeline/spec.md):\n * hover ripple scaleX 2.6/1.7/1.25 with opacity 1/.86/.72/.58 over 150ms,\n * 320px preview cards after a 120ms delay, prefers-reduced-motion fallback,\n * full aria labelling.\n *\n * Geometry follows the native TurnNavigator model: the vertical band\n * (--dsh-tl-band, the host's viewport-minus-composer rule) is computed in CSS\n * from the host variables that the seat inherits, so no JS measures it. Only\n * the horizontal left edge is measured — the conversation column is a grid\n * track, so it is not flush to the window edge once the right sidebar opens.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport { type TimelineItem } from './directory.ts'\nimport type { TimelineKey } from './locales.ts'\nimport { jumpToTurn, probeChatDom, type ChatDomProbe } from './dom.ts'\nimport { ensureStyles, removeStyles } from './styles.ts'\nimport type { TimelineSource } from './source.ts'\n\n/** Component props: the locale seat, the seat's session id, and the source. */\nexport type TimelineRailProps = PropsLocale<'chat-timeline'> & {\n /** The session this rail belongs to (host-supplied by the seat's inject). */\n sessionId: string\n /** That session's snapshot source (built from ctx.sessions). */\n source: TimelineSource\n}\n\n/** Slot height per tick (10px pitch, ZCode parity). */\nconst ITEM_PITCH = 10\n/** Preview-card hover delays (ms). */\nconst TIP_OPEN_DELAY = 120\nconst TIP_CLOSE_DELAY = 80\n\n/** Ripple visual per distance from the hovered tick (ZCode _5e parity).\n * The ripple is interaction-only: at rest every tick is idle (scaleX 1),\n * and the current position is signalled by color/opacity, not length. */\nfunction ripple(distance: number): { opacity: number; scaleX: number; tone: 'peak' | 'near' | 'mid' | 'idle' } {\n if (distance === 0) return { opacity: 1, scaleX: 2.6, tone: 'peak' }\n if (distance === 1) return { opacity: 0.86, scaleX: 1.7, tone: 'near' }\n if (distance === 2) return { opacity: 0.72, scaleX: 1.25, tone: 'mid' }\n return { opacity: 0.58, scaleX: 1, tone: 'idle' }\n}\n\n/** Tick colors: foreground for the hovered peak and the current turn at rest,\n * the subtlest text tone otherwise (ZCode bg-foreground / -subtlest parity\n * through system colors). */\nconst TICK_COLOR_FOREGROUND = 'CanvasText'\nconst TICK_COLOR_SUBTLE = 'color-mix(in srgb, CanvasText 42%, transparent)'\n\n/**\n * Render the question navigator rail.\n * @param props - composed slot props.\n * @returns the rail, or null when it should not render.\n */\nexport function TimelineRail({ sessionId, source, t }: TimelineRailProps) {\n const state = useSyncExternalStore(source.subscribe, source.getSnapshot)\n const probe = useMemo<ChatDomProbe | null>(() => (typeof document === 'undefined' ? null : probeChatDom(document)), [])\n const [container, setContainer] = useState<HTMLElement | null>(null)\n const [navEl, setNavEl] = useState<HTMLElement | null>(null)\n const [activeIndex, setActiveIndex] = useState(-1)\n const [hoverIndex, setHoverIndex] = useState<number | undefined>(undefined)\n const [tipIndex, setTipIndex] = useState<number | undefined>(undefined)\n const timers = useRef<{ open: number | undefined; close: number | undefined }>({ open: undefined, close: undefined })\n const frame = useRef(0)\n\n const snapshot = state.snapshot\n // 官方 turnOutline 数据面:source 直接产出刻度(宿主已算好全史预览)。\n const items = useMemo<TimelineItem[]>(\n () => (snapshot === null ? [] : (snapshot.items as TimelineItem[])),\n [snapshot],\n )\n const sourceRef = useRef(source)\n sourceRef.current = source\n const itemsRef = useRef<TimelineItem[]>([])\n itemsRef.current = items\n // 原生锚点:行按 `data-chat-turn=\"<turn>\"` 键控(dsh-client-ui-chat ChatView),\n // 跳转与高亮都按轮次号走原生逻辑;seq 仅用于 loadThrough 翻页。\n const seqs = useMemo(() => items.map((item) => item.seq), [items])\n const seqsRef = useRef<number[]>([])\n seqsRef.current = seqs\n const turns = useMemo(() => items.map((item) => item.turn), [items])\n const turnsRef = useRef<(number | undefined)[]>([])\n turnsRef.current = turns\n\n useEffect(() => {\n if (typeof document === 'undefined') return\n ensureStyles(document)\n // 卸载时移除样式表:隐藏原生 Turn 导航的规则随标签消失,原生立即恢复。\n return () => removeStyles(document)\n }, [])\n\n // Locate the chat scrollport. The seat renders as soon as the session scope\n // exists, which can be a beat before the conversation itself mounts.\n useEffect(() => {\n if (probe === null) return\n let attempts = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n const tick = () => {\n const found = probe.getContainer()\n if (found !== null || attempts >= 10) {\n setContainer(found)\n return\n }\n attempts += 1\n timer = setTimeout(tick, 300)\n }\n tick()\n return () => {\n if (timer !== undefined) clearTimeout(timer)\n }\n }, [probe, sessionId])\n\n // Horizontal geometry only: the rail's left edge, which is the conversation\n // column's left edge. Vertical geometry is pure CSS (styles.ts) because the\n // band variables live on the scrollport and the seat inherits them; the\n // column stays measured because it is a grid track that the right sidebar\n // shrinks, so it is not derivable from a window-edge offset.\n useEffect(() => {\n if (container === null || navEl === null) return\n const write = () => {\n navEl.style.setProperty('--dsh-tl-left', `${container.getBoundingClientRect().left}px`)\n }\n write()\n const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(write)\n observer?.observe(container)\n window.addEventListener('resize', write)\n return () => {\n observer?.disconnect()\n window.removeEventListener('resize', write)\n }\n }, [container, navEl])\n\n // Active-tick highlight follows the conversation's scrolling.\n useEffect(() => {\n if (container === null || probe === null) return\n const sync = () => {\n if (frame.current !== 0) return\n frame.current = requestAnimationFrame(() => {\n frame.current = 0\n setActiveIndex(probe.activeIndex(turnsRef.current))\n })\n }\n sync()\n container.addEventListener('scroll', sync, { passive: true })\n const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(sync)\n observer?.observe(container)\n window.addEventListener('resize', sync)\n return () => {\n container.removeEventListener('scroll', sync)\n observer?.disconnect()\n window.removeEventListener('resize', sync)\n if (frame.current !== 0) cancelAnimationFrame(frame.current)\n frame.current = 0\n }\n }, [container, probe])\n\n // items 变化(事件窗口增长/切换会话)后重算一次 active 高亮。\n useEffect(() => {\n if (container === null || probe === null) return\n setActiveIndex(probe.activeIndex(turns))\n }, [container, probe, turns])\n\n // 完全模仿原生:只要会话可读就渲染——不设条数/宽度门槛,原生导航条的隐藏\n // 完全交给本插件的接管规则。\n const visible = snapshot !== null\n // The ripple follows the pointer only (ZCode v5e parity): at rest every tick\n // is equal length and the current turn stands out by color/opacity alone.\n const focusIndex = hoverIndex\n\n const scheduleTip = useCallback((index: number | undefined) => {\n if (timers.current.open !== undefined) window.clearTimeout(timers.current.open)\n if (timers.current.close !== undefined) window.clearTimeout(timers.current.close)\n if (index === undefined) {\n timers.current.close = window.setTimeout(() => setTipIndex(undefined), TIP_CLOSE_DELAY)\n return\n }\n timers.current.open = window.setTimeout(() => setTipIndex(index), TIP_OPEN_DELAY)\n }, [])\n\n useEffect(() => () => {\n if (timers.current.open !== undefined) window.clearTimeout(timers.current.open)\n if (timers.current.close !== undefined) window.clearTimeout(timers.current.close)\n }, [])\n\n const jump = useCallback((index: number) => {\n // 原生 navigateToTurn 流程:行已挂载直接落位;未挂载走官方 loadThrough\n // 翻页后落位。落位是瞬时滚动(原生 landOnRow 语义),无需 motion 分支。\n void jumpToTurn(\n document,\n index,\n turnsRef.current,\n seqsRef.current,\n (seq: number) => sourceRef.current?.jumpThrough(seq) ?? Promise.resolve(),\n )\n }, [])\n\n if (!visible || probe === null) return null\n\n const trackHeight = items.length * ITEM_PITCH\n const tip = tipIndex !== undefined ? items[tipIndex] : undefined\n\n return (\n <>\n <nav\n ref={setNavEl}\n aria-label={t('nav.label')}\n className=\"dsh-tl-nav\"\n data-visible={visible ? 'true' : 'false'}\n data-testid=\"dsh-chat-timeline\"\n data-item-count={items.length}\n >\n <div\n className=\"dsh-tl-scroll\"\n onPointerLeave={() => {\n setHoverIndex(undefined)\n scheduleTip(undefined)\n }}\n >\n <div className=\"dsh-tl-track\" style={{ height: `${trackHeight}px` }}>\n {items.map((item, index) => {\n const isActive = index === activeIndex\n const distance = focusIndex === undefined ? 3 : Math.abs(index - focusIndex)\n const look = ripple(distance)\n const activeAtRest = focusIndex === undefined && isActive\n const foreground = look.tone === 'peak' || activeAtRest\n const opacity = activeAtRest\n ? 0.9\n : item.running\n ? Math.max(look.opacity, 0.72)\n : look.opacity\n return (\n <div\n key={item.key}\n className=\"dsh-tl-slot\"\n style={{ transform: `translateY(${index * ITEM_PITCH}px)` }}\n onMouseEnter={() => {\n setHoverIndex(index)\n scheduleTip(index)\n }}\n onFocus={() => {\n setHoverIndex(index)\n scheduleTip(index)\n }}\n onBlur={() => {\n setHoverIndex(undefined)\n scheduleTip(undefined)\n }}\n >\n <button\n type=\"button\"\n aria-current={isActive ? 'location' : undefined}\n aria-label={t('nav.jumpToQuery', { index: String(index + 1) })}\n aria-posinset={index + 1}\n aria-setsize={items.length}\n data-testid=\"dsh-chat-timeline-item\"\n data-item-index={index}\n data-active={isActive ? 'true' : 'false'}\n data-running={item.running ? 'true' : 'false'}\n onClick={() => jump(index)}\n className=\"dsh-tl-slot-inner\"\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-start',\n width: '100%',\n height: '100%',\n padding: 0,\n border: 0,\n background: 'transparent',\n cursor: 'pointer',\n }}\n >\n <span\n className=\"dsh-tl-tick\"\n style={{\n opacity,\n transform: `scaleX(${look.scaleX})`,\n backgroundColor: foreground ? TICK_COLOR_FOREGROUND : TICK_COLOR_SUBTLE,\n }}\n />\n </button>\n </div>\n )\n })}\n </div>\n </div>\n </nav>\n {tip !== undefined && tipIndex !== undefined\n ? createPortal(\n <TipCard item={tip} index={tipIndex} nav={navEl} t={t} />,\n document.body,\n )\n : null}\n </>\n )\n}\n\nfunction TipCard({ item, index, nav, t }: {\n item: TimelineItem\n index: number\n nav: HTMLElement | null\n t: (key: TimelineKey, params?: Record<string, string | number>) => string\n}) {\n const slot = nav?.querySelector<HTMLElement>('[data-testid=\"dsh-chat-timeline-item\"][data-item-index=\"' + String(index) + '\"]')\n const box = slot?.getBoundingClientRect()\n const railBox = nav?.getBoundingClientRect()\n const left = box !== undefined\n ? Math.min(box.right + 8, window.innerWidth - 336)\n : (railBox !== undefined ? railBox.right + 8 : 8)\n const top = box !== undefined ? Math.max(8, Math.min(box.top - 4, window.innerHeight - 160)) : 8\n return (\n <div\n className=\"dsh-tl-tip\"\n data-testid=\"dsh-chat-timeline-tip\"\n style={{ left, top }}\n role=\"tooltip\"\n >\n <p className=\"dsh-tl-tip-user\">{item.userFallback ? t('preview.userFallback') : item.userPreview}</p>\n <p className=\"dsh-tl-tip-assistant\" data-kind={item.assistantKind}>\n {item.assistantKind === 'running'\n ? t('preview.assistantRunning')\n : item.assistantKind === 'empty'\n ? t('preview.assistantEmpty')\n : item.assistantPreview}\n </p>\n </div>\n )\n}\n","/**\n * Timeline source: follows the current session through the cordis services\n * face (ctx.sessions) and exposes a useSyncExternalStore-compatible read\n * face over the host-computed turn outline. Client context usage here is\n * type-only; at runtime everything is reached through the ctx services, so\n * the client bundle stays free of cross-package value imports.\n */\n// DSH-0.1.2-A1-25: dsh-client-runtime 包已删除,ClientContext 即 cordis Context。\n// ctx.sessions 的 Context 合并自 session-controller 的 client 入口(A1-25 迁移表)。\nimport type {} from '@deepseek-ai/dsh-api-session-controller/client'\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport { buildPreview, MAX_PREVIEW_CHARS, MAX_PREVIEW_PARAGRAPHS, type TimelineItem } from './directory.ts'\n\n/**\n * The slice of the turn outline the timeline consumes — 官方 `turnOutline`\n * 投影(session-turn-outline 域)的 wire 条目:宿主对整份日志 fold 出的\n * 全史轮次索引,无需客户端翻页。\n */\nexport interface TurnOutlineEntryLike {\n readonly turn: number\n readonly seq: number\n readonly prompt?: string\n readonly response?: string\n}\n\n/** The slice of ConversationSnapshot the timeline consumes. */\nexport interface TimelineSnapshot {\n readonly sessionId: string\n /** One tick per known turn, ascending(官方 outline 顺序)。 */\n readonly items: readonly TimelineItem[]\n /** Whether the session currently has a running turn. */\n readonly running: boolean\n}\n\n/** State handed to React: null snapshot means \"no readable session right now\". */\nexport interface TimelineState {\n readonly sessionId: string | undefined\n readonly snapshot: TimelineSnapshot | null\n}\n\n/** useSyncExternalStore-compatible source face. */\nexport interface TimelineSource {\n subscribe(listener: () => void): () => void\n getSnapshot(): TimelineState\n /**\n * Page the history window back through the given turn's seq(官方\n * `loadThrough` turn-jump loader),so the turn's rows mount and the DOM\n * probe can scroll to them. No-op when the session face lacks the verb.\n */\n jumpThrough(seq: number): Promise<void>\n}\n\ninterface ObservableLike<T> {\n getSnapshot(): T\n subscribe(listener: () => void): () => void\n}\n\n/**\n * Structural face of ctx.sessions this module needs (kept narrow for tests).\n * 0.1.7: the session id is handed in by the host (session-scoped seat inject),\n * `binding(id)` borrows the already-retained scope, `projections.faceOf\n * ('turnOutline')` is the official whole-log turn index, and `loadThrough(seq)`\n * is the turn-jump pagination verb.\n */\nexport interface SessionsLike {\n binding(\n id: string,\n ): {\n session: ObservableLike<{ running?: boolean }> & {\n loadThrough?(seq: number): Promise<void>\n projections?: {\n faceOf?(key: string): ObservableSnapshotLike<unknown>\n }\n }\n eventSource: ObservableLike<unknown>\n } | undefined\n}\n\n/** ObservableSnapshot 的最小结构面(getSnapshot/subscribe)。 */\ntype ObservableSnapshotLike<T> = ObservableLike<T>\n\n/** Project one outline entry onto a tick (previews are host-bounded already). */\nexport function itemFromOutline(\n entry: TurnOutlineEntryLike,\n isLive: boolean,\n labels: { userFallback: string; assistantRunning: string; assistantEmpty: string },\n): TimelineItem {\n const prompt = typeof entry.prompt === 'string' ? entry.prompt : ''\n const response = typeof entry.response === 'string' ? entry.response : ''\n const assistantKind: TimelineItem['assistantKind'] = response\n ? 'text'\n : isLive\n ? 'running'\n : 'empty'\n return {\n key: `t${entry.turn}`,\n seq: entry.seq,\n time: 0,\n turn: entry.turn,\n userPreview: prompt || labels.userFallback,\n userFallback: !prompt,\n assistantPreview: response || (isLive ? labels.assistantRunning : labels.assistantEmpty),\n assistantKind,\n running: isLive,\n }\n}\n\n/** Build the tick list from the outline plus the live running flag. */\nexport function itemsFromOutline(\n outline: readonly TurnOutlineEntryLike[],\n running: boolean,\n labels: { userFallback: string; assistantRunning: string; assistantEmpty: string },\n): TimelineItem[] {\n const last = outline.length - 1\n return outline.map((entry, index) =>\n itemFromOutline(entry, running && index === last, labels),\n )\n}\n\n// 保留目录数学的导出面(rail 不再走 nodes 路径,preview 预算常量仍被引用)。\nexport { buildPreview, MAX_PREVIEW_CHARS, MAX_PREVIEW_PARAGRAPHS }\n\n/** Fallback preview labels when the outline entry carries no text. */\nconst PREVIEW_LABELS = {\n userFallback: '(无文本)',\n assistantRunning: '(运行中)',\n assistantEmpty: '(空)',\n} as const\n\n/**\n * Create the source for one session. The id is supplied by the host — the\n * session-scoped seat hands it to `inject` — so this no longer guesses which\n * session is open and no longer watches the sessions list: it binds straight\n * to that session's outline projection and republishes whenever the outline or\n * the running flag moves.\n *\n * A bounded retry covers the materialization window (the seat can render a\n * beat before `binding(id)` answers); after that the rail stays hidden.\n * @param sessions - the sessions service face (pass ctx.sessions).\n * @param sessionId - the session this rail belongs to.\n * @returns the source face.\n */\nexport function createTimelineSource(sessions: SessionsLike, sessionId: string): TimelineSource {\n let state: TimelineState = { sessionId, snapshot: null }\n const listeners = new Set<() => void>()\n let subscribed = false\n let retryTimer: ReturnType<typeof setTimeout> | undefined\n let retryAttempts = 0\n\n const emit = () => {\n for (const listener of listeners) listener()\n }\n\n const readBinding = () => {\n try {\n return sessions.binding(sessionId)\n } catch {\n return undefined\n }\n }\n\n const bind = () => {\n if (retryTimer !== undefined) {\n clearTimeout(retryTimer)\n retryTimer = undefined\n }\n const session = readBinding()?.session\n const outlineFace = (() => {\n try {\n return session?.projections?.faceOf?.('turnOutline') as\n | ObservableLike<readonly TurnOutlineEntryLike[]>\n | undefined\n } catch {\n return undefined\n }\n })()\n if (!session || !outlineFace) {\n // Scope still materializing: keep an empty state and retry with backoff\n // so the rail appears once the binding lands.\n state = { sessionId, snapshot: null }\n emit()\n if (subscribed && retryAttempts < 10) {\n retryAttempts += 1\n retryTimer = setTimeout(bind, 250 * retryAttempts)\n }\n return\n }\n retryAttempts = 0\n const push = () => {\n try {\n const outline = outlineFace.getSnapshot()\n const running = Boolean(session.getSnapshot()?.running)\n state = {\n sessionId,\n snapshot: {\n sessionId,\n items: Array.isArray(outline)\n ? itemsFromOutline(outline, running, PREVIEW_LABELS)\n : [],\n running,\n },\n }\n } catch {\n state = { sessionId, snapshot: null }\n }\n emit()\n }\n push()\n try {\n outlineFace.subscribe(push)\n session.subscribe(push)\n } catch {\n // Observable faces expose no unsubscribe disposer contract here; the\n // subscription rides the plugin fiber and is torn down with it.\n }\n }\n\n return {\n subscribe(listener: () => void): () => void {\n const first = listeners.size === 0\n listeners.add(listener)\n if (first) {\n subscribed = true\n bind()\n }\n return () => {\n listeners.delete(listener)\n }\n },\n getSnapshot(): TimelineState {\n return state\n },\n jumpThrough(seq: number): Promise<void> {\n const session = readBinding()?.session\n if (typeof session?.loadThrough !== 'function') return Promise.resolve()\n return session.loadThrough(seq).catch(() => {})\n },\n }\n}\n\n/** Build the source from the plugin client context and the seat's session id. */\nexport function timelineSourceFromContext(ctx: ClientContext, sessionId: string): TimelineSource {\n return createTimelineSource(ctx.sessions as unknown as SessionsLike, sessionId)\n}\n","/**\n * Locale dictionaries for the chat-timeline plugin. `zh` is the key-set source\n * of truth; `en` keeps a full key-for-key mirror. Registered through\n * ctx.locale.register(NS, { zh, en }).\n */\n\n/** Simplified Chinese dictionary (key-set source of truth). */\nexport const zh = {\n 'nav.label': '对话问题导航',\n 'nav.jumpToQuery': '跳转到第 {index} 条问题',\n 'preview.userFallback': '用户输入',\n 'preview.assistantEmpty': '暂无助手正文',\n 'preview.assistantRunning': '助手仍在工作',\n}\n\n/** The chat-timeline namespace key union. */\nexport type TimelineKey = keyof typeof zh\n\n/** English dictionary, key-for-key complete against zh. */\nexport const en: Record<TimelineKey, string> = {\n 'nav.label': 'Conversation question navigator',\n 'nav.jumpToQuery': 'Jump to question {index}',\n 'preview.userFallback': 'User input',\n 'preview.assistantEmpty': 'No assistant text yet',\n 'preview.assistantRunning': 'Assistant is still working',\n}\n","/**\n * Chat-timeline plugin — browser half. Registers the `chat-timeline` locale\n * dictionaries and a `shell.overlay` entry that renders the question\n * navigator rail beside the conversation. Export discipline: the /client\n * surface carries only what cordis loading needs plus types.\n */\n// DSH-0.1.2-A1-25: dsh-client-runtime 包已删除,ClientContext 即 cordis Context。\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale) and its\n// LocaleNamespaceMap merge table.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ui-layout SlotMap merge (the 'shell.overlay' hole).\nimport type {} from '@deepseek-ai/dsh-client-ui-layout/client'\n// Type-only: pulls the ui-conversation SlotMap merge (the\n// 'conversation.input.overlay' hole the rail registers into).\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n// DSH-0.1.5-A1-14/A2-13: ctx.slots 的 Context 合并自 ui-renderer 的 client 入口。\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport { TimelineRail } from './rail.tsx'\nimport { timelineSourceFromContext } from './source.ts'\nimport { en, zh, type TimelineKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Chat-timeline surface copy. */\n 'chat-timeline': TimelineKey\n }\n}\n\n/** Dictionary namespace owned by this plugin. */\nconst NS = 'chat-timeline'\n\n/**\n * Session-scoped seat the rail occupies.\n *\n * DSH 0.1.7 moved \"which session is open\" out of the sessions service: the list\n * snapshot no longer carries `current`, `binding(id)` only borrows an already\n * retained scope, and session-controller states outright that view selection\n * stays outside the Controller. The only supported way to learn the open\n * session is a session-scoped seat, whose `inject` receives the scope\n * binding's key — the session id. A root-scope seat (shell.overlay) cannot be\n * told, which is why the rail no longer lives there.\n *\n * `conversation.input.overlay` is rendered for every open session, and unlike\n * `conversation.view` it is not enumerated into the conversation's view tabs.\n * It also sits inside the conversation scroller, so the host's geometry\n * variables (--dsh-conversation-viewport-height / --dsh-composer-height)\n * inherit to the rail.\n */\nconst SEAT = 'conversation.input.overlay'\n\n/** Unique occupant id inside the shared conversation.input.overlay list slot. */\nconst ENTRY_ID = 'chat-timeline'\n\n/** Services required by this plugin. */\nexport const inject = ['slots', 'locale', 'sessions']\n\n/**\n * Register the timeline surface.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => {\n try {\n return ctx.locale.register(NS, { zh, en })\n } catch {\n return () => {}\n }\n }, 'chat-timeline: dictionaries')\n\n // The rail is a frame-wide overlay, but it is registered on a session-scoped\n // seat: that is the only channel through which 0.1.7 hands out the open\n // session id. Registration is declaration-aware via slots.inject. 注册失败\n // 降级为一条 console 错误(不抛进 loader,也不静默——留诊断线索)。\n ctx.slots.inject(SEAT, () => {\n try {\n return ctx.slots.register({\n name: SEAT,\n id: ENTRY_ID,\n order: 0,\n locale: NS,\n // Session-scoped registrations receive the scope binding's key, i.e.\n // the session id (renderer runInject pushes binding.key).\n inject: (sessionId: string) => ({\n sessionId,\n source: timelineSourceFromContext(ctx, sessionId),\n }),\n }, TimelineRail)\n } catch (err) {\n console.error(`chat-timeline: ${SEAT} register failed:`, err)\n return () => {}\n }\n })\n}\n\nexport type { TimelineRailProps } from './rail.tsx'\nexport type { TimelineKey } from './locales.ts'\nexport type { TimelineSource, TimelineSnapshot, TimelineState } from './source.ts'\n"],"mappings":"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"}
@@ -20,6 +20,8 @@ export interface TimelineNodeLike {
20
20
  content?: readonly unknown[];
21
21
  /** Assistant content blocks (each `{ kind, text? }`-like). */
22
22
  blocks?: readonly unknown[];
23
+ /** Host turn number when known (0.1.5 DOM anchors key off `turn:N`). */
24
+ turn?: number;
23
25
  }
24
26
  /** One tick in the timeline directory. */
25
27
  export interface TimelineItem {
@@ -29,6 +31,8 @@ export interface TimelineItem {
29
31
  readonly seq: number;
30
32
  /** Anchor time (epoch ms); 0 when unknown. */
31
33
  readonly time: number;
34
+ /** Host turn number when known (drives the DOM anchor lookup). */
35
+ readonly turn?: number;
32
36
  /** Trimmed human input preview. */
33
37
  readonly userPreview: string;
34
38
  /** True when the human node carried no extractable text. */
@@ -1,38 +1,71 @@
1
1
  /**
2
2
  * Chat DOM probe — the only module allowed to know DSH's internal chat DOM.
3
3
  *
4
- * Contract measured on dsh 0.1.1-rc.2 (see .scratch/chat-timeline/issues/03):
5
- * the scroll container carries `data-conversation-scroll`; each flow row is a
6
- * `[data-chat-flow-kind]` element whose `user` kind marks a human turn, in the
7
- * same seq order as the snapshot's user nodes. Every access is defensive:
8
- * when detection fails the probe degrades to a no-op rail (no jump, no
9
- * highlight) instead of throwing.
4
+ * Contract replicated 1:1 from the native chat view (dsh-client-ui-chat
5
+ * 0.1.5-rc.2, ChatView):
6
+ * - every rendered row carries `data-chat-turn="<turn>"` (plus
7
+ * `data-chat-anchor-key` / `data-chat-flow-key` with the node key, which is
8
+ * NOT seq-prefixed — never parse seqs out of flow keys);
9
+ * - the scrollport is `row.closest('[data-conversation-scroll]')`;
10
+ * - landing on a turn is an instant `el.scrollTop += flowTop(row, el) - 24`;
11
+ * - a turn outside the loaded event window is paged in through the official
12
+ * `session.loadThrough(turn/start seq)` verb, then landed once its row
13
+ * renders (the native pendingJump/settle loop, plugin-side as a poll);
14
+ * - the active turn reads the line `top + min(96, height * 0.2)` via
15
+ * `elementsFromPoint` hit-testing with a row-scan fallback, and the last
16
+ * turn wins within 25px of the scroll bottom.
17
+ * Every access is defensive: when detection fails the probe degrades to a
18
+ * no-op rail (no jump, no highlight) instead of throwing.
10
19
  */
11
20
  /** Read-only view of the chat DOM the rail interacts with. */
12
21
  export interface ChatDomProbe {
13
22
  /** The chat scroll container, or null when not found. */
14
23
  getContainer(): HTMLElement | null;
15
- /** Human-turn row elements in document order (user kind only). */
16
- getUserRows(): HTMLElement[];
17
- /** Index of the human turn nearest the viewport top, or -1. */
18
- activeIndex(): number;
19
- /** Scroll the given human turn into view; false when unavailable. */
20
- jumpTo(index: number, behavior: ScrollBehavior): boolean;
24
+ /** Index of the tick nearest the reading line, or -1. */
25
+ activeIndex(turns: readonly (number | undefined)[]): number;
21
26
  }
22
27
  /**
23
28
  * Find the chat scroll container with a candidate chain.
24
29
  * 1. the explicit stable hook `[data-conversation-scroll]`;
25
30
  * 2. the closest scrollable ancestor of the flow column `[data-chat-flow]`;
26
- * 3. the largest scrollable element that contains human rows.
31
+ * 3. the largest scrollable element that contains keyed flow items.
27
32
  */
28
33
  export declare function findChatContainer(doc: Document): HTMLElement | null;
29
- /** Human-turn rows inside the container (falling back to a document-wide query). */
30
- export declare function findUserRows(container: HTMLElement | null): HTMLElement[];
31
34
  /**
32
- * The active index: the last human row whose top edge sits above the
33
- * container's reading line (40px below the top edge), matching the ZCode
34
- * "unit at viewport top" rule; clamps to the last row at scroll bottom.
35
+ * The rendered row of `turn` (native anchor attribute `data-chat-turn`),
36
+ * skipping hidden rows like the native `anchorElement`. Scoped to the chat
37
+ * container when it is already located (native scopes to its own list), so a
38
+ * stray second mount cannot hijack the anchor. Null when the turn's events
39
+ * are not in the loaded window yet.
35
40
  */
36
- export declare function computeActiveIndex(container: HTMLElement, rows: readonly HTMLElement[]): number;
41
+ export declare function findTurnRow(doc: Document, turn: number | undefined): HTMLElement | null;
42
+ /**
43
+ * Native `landOnRow`: instant scroll so the row's top sits 24px below the
44
+ * scrollport top. Returns false when the scrollport cannot be resolved.
45
+ */
46
+ export declare function landOnRow(doc: Document, row: HTMLElement): boolean;
47
+ /**
48
+ * Native `turnAtLine`: the turn owning the row at a scrollport line.
49
+ * Hit-tests the line first (elementsFromPoint at the container's horizontal
50
+ * center) and falls back to one row scan when layout cannot answer (jsdom,
51
+ * pre-paint). Returns null when no loaded row covers the line.
52
+ */
53
+ export declare function turnAtLine(doc: Document, container: HTMLElement, line: number): number | null;
54
+ /**
55
+ * The active index (native `syncActiveTurn`): within 25px of the scroll
56
+ * bottom the last tick wins; otherwise the reading line sits at
57
+ * `top + min(96, height * 0.2)` and the active tick is the last one whose
58
+ * turn does not exceed the turn read at that line. When the line covers no
59
+ * loaded row, earlier ticks outside the window count as above the viewport
60
+ * (the window head's predecessor is active, matching 0.1.1 parity).
61
+ */
62
+ export declare function computeActiveIndex(container: HTMLElement, doc: Document, turns: readonly (number | undefined)[]): number;
63
+ /**
64
+ * 跳到第 index 个刻度的完整落点流程(原生 navigateToTurn 的插件侧等价物):
65
+ * 1. 按 `data-chat-turn` 找行并原生落位(瞬时、行顶距视口顶 24px);
66
+ * 2. 行未挂载时先 `loadThrough(turn/start 的 seq)` 把事件窗口翻页到位;
67
+ * 3. 轮询等行渲染后落位。任何一步成功即返回 true。
68
+ */
69
+ export declare function jumpToTurn(doc: Document, index: number, turns: readonly (number | undefined)[], seqs: readonly number[], loadThrough: ((seq: number) => Promise<void>) | undefined): Promise<boolean>;
37
70
  /** Create the probe against a document (injectable for tests). */
38
71
  export declare function probeChatDom(doc: Document): ChatDomProbe;
@@ -4,7 +4,7 @@
4
4
  * navigator rail beside the conversation. Export discipline: the /client
5
5
  * surface carries only what cordis loading needs plus types.
6
6
  */
7
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
7
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
8
8
  import { type TimelineKey } from './locales.ts';
9
9
  declare module '@deepseek-ai/dsh-client-ui-slots' {
10
10
  interface LocaleNamespaceMap {
@@ -1,8 +1,10 @@
1
1
  import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
2
2
  import type { TimelineSource } from './source.ts';
3
- /** Component props: the locale seat plus the injected session source. */
3
+ /** Component props: the locale seat, the seat's session id, and the source. */
4
4
  export type TimelineRailProps = PropsLocale<'chat-timeline'> & {
5
- /** The current-session snapshot source (built from ctx.sessions). */
5
+ /** The session this rail belongs to (host-supplied by the seat's inject). */
6
+ sessionId: string;
7
+ /** That session's snapshot source (built from ctx.sessions). */
6
8
  source: TimelineSource;
7
9
  };
8
10
  /**
@@ -10,4 +12,4 @@ export type TimelineRailProps = PropsLocale<'chat-timeline'> & {
10
12
  * @param props - composed slot props.
11
13
  * @returns the rail, or null when it should not render.
12
14
  */
13
- export declare function TimelineRail({ source, t }: TimelineRailProps): import("react").JSX.Element | null;
15
+ export declare function TimelineRail({ sessionId, source, t }: TimelineRailProps): import("react").JSX.Element | null;
@@ -1,16 +1,21 @@
1
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
2
+ import { buildPreview, MAX_PREVIEW_CHARS, MAX_PREVIEW_PARAGRAPHS, type TimelineItem } from './directory.ts';
1
3
  /**
2
- * Timeline source: follows the current session through the cordis services
3
- * face (ctx.sessions) and exposes a useSyncExternalStore-compatible read
4
- * face over its ConversationSnapshot. All dsh-client-runtime usage here is
5
- * type-only; at runtime everything is reached through the ctx services, so
6
- * the client bundle stays free of cross-package value imports.
4
+ * The slice of the turn outline the timeline consumes — 官方 `turnOutline`
5
+ * 投影(session-turn-outline 域)的 wire 条目:宿主对整份日志 fold 出的
6
+ * 全史轮次索引,无需客户端翻页。
7
7
  */
8
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
8
+ export interface TurnOutlineEntryLike {
9
+ readonly turn: number;
10
+ readonly seq: number;
11
+ readonly prompt?: string;
12
+ readonly response?: string;
13
+ }
9
14
  /** The slice of ConversationSnapshot the timeline consumes. */
10
15
  export interface TimelineSnapshot {
11
16
  readonly sessionId: string;
12
- /** Ordered conversation nodes (the snapshot's legacy compatibility field). */
13
- readonly nodes: readonly unknown[];
17
+ /** One tick per known turn, ascending(官方 outline 顺序)。 */
18
+ readonly items: readonly TimelineItem[];
14
19
  /** Whether the session currently has a running turn. */
15
20
  readonly running: boolean;
16
21
  }
@@ -23,31 +28,65 @@ export interface TimelineState {
23
28
  export interface TimelineSource {
24
29
  subscribe(listener: () => void): () => void;
25
30
  getSnapshot(): TimelineState;
31
+ /**
32
+ * Page the history window back through the given turn's seq(官方
33
+ * `loadThrough` turn-jump loader),so the turn's rows mount and the DOM
34
+ * probe can scroll to them. No-op when the session face lacks the verb.
35
+ */
36
+ jumpThrough(seq: number): Promise<void>;
26
37
  }
27
38
  interface ObservableLike<T> {
28
39
  getSnapshot(): T;
29
40
  subscribe(listener: () => void): () => void;
30
41
  }
31
- /** Structural face of ctx.sessions this module needs (kept narrow for tests). */
42
+ /**
43
+ * Structural face of ctx.sessions this module needs (kept narrow for tests).
44
+ * 0.1.7: the session id is handed in by the host (session-scoped seat inject),
45
+ * `binding(id)` borrows the already-retained scope, `projections.faceOf
46
+ * ('turnOutline')` is the official whole-log turn index, and `loadThrough(seq)`
47
+ * is the turn-jump pagination verb.
48
+ */
32
49
  export interface SessionsLike {
33
- readonly list: ObservableLike<{
34
- current?: string;
35
- }>;
36
50
  binding(id: string): {
37
51
  session: ObservableLike<{
38
- nodes?: readonly unknown[];
39
52
  running?: boolean;
40
- }>;
53
+ }> & {
54
+ loadThrough?(seq: number): Promise<void>;
55
+ projections?: {
56
+ faceOf?(key: string): ObservableSnapshotLike<unknown>;
57
+ };
58
+ };
59
+ eventSource: ObservableLike<unknown>;
41
60
  } | undefined;
42
61
  }
62
+ /** ObservableSnapshot 的最小结构面(getSnapshot/subscribe)。 */
63
+ type ObservableSnapshotLike<T> = ObservableLike<T>;
64
+ /** Project one outline entry onto a tick (previews are host-bounded already). */
65
+ export declare function itemFromOutline(entry: TurnOutlineEntryLike, isLive: boolean, labels: {
66
+ userFallback: string;
67
+ assistantRunning: string;
68
+ assistantEmpty: string;
69
+ }): TimelineItem;
70
+ /** Build the tick list from the outline plus the live running flag. */
71
+ export declare function itemsFromOutline(outline: readonly TurnOutlineEntryLike[], running: boolean, labels: {
72
+ userFallback: string;
73
+ assistantRunning: string;
74
+ assistantEmpty: string;
75
+ }): TimelineItem[];
76
+ export { buildPreview, MAX_PREVIEW_CHARS, MAX_PREVIEW_PARAGRAPHS };
43
77
  /**
44
- * Create the source. Subscribes to the sessions list, rebinds to the current
45
- * session's snapshot feed on selection change, and republishes a stable state
46
- * object whenever the underlying snapshot changes.
78
+ * Create the source for one session. The id is supplied by the host — the
79
+ * session-scoped seat hands it to `inject` — so this no longer guesses which
80
+ * session is open and no longer watches the sessions list: it binds straight
81
+ * to that session's outline projection and republishes whenever the outline or
82
+ * the running flag moves.
83
+ *
84
+ * A bounded retry covers the materialization window (the seat can render a
85
+ * beat before `binding(id)` answers); after that the rail stays hidden.
47
86
  * @param sessions - the sessions service face (pass ctx.sessions).
87
+ * @param sessionId - the session this rail belongs to.
48
88
  * @returns the source face.
49
89
  */
50
- export declare function createTimelineSource(sessions: SessionsLike): TimelineSource;
51
- /** Build the source from the plugin client context. */
52
- export declare function timelineSourceFromContext(ctx: ClientContext): TimelineSource;
53
- export {};
90
+ export declare function createTimelineSource(sessions: SessionsLike, sessionId: string): TimelineSource;
91
+ /** Build the source from the plugin client context and the seat's session id. */
92
+ export declare function timelineSourceFromContext(ctx: ClientContext, sessionId: string): TimelineSource;
@@ -5,6 +5,8 @@
5
5
  * rail follows the app's color-scheme without reaching into DSH theme tokens.
6
6
  * Visual constants replicate the ZCode TurnNavigator (spec section 2.2).
7
7
  */
8
- export declare const TIMELINE_STYLES = "\n.dsh-tl-nav {\n position: fixed;\n z-index: 10;\n width: 48px;\n pointer-events: none;\n opacity: 1;\n transition: opacity 150ms ease-out;\n}\n.dsh-tl-nav[data-visible=\"false\"] {\n opacity: 0;\n visibility: hidden;\n}\n.dsh-tl-scroll {\n position: absolute;\n left: 12px;\n top: 50%;\n transform: translateY(-50%);\n width: 36px;\n max-height: calc(100% - 96px);\n overflow-x: hidden;\n overflow-y: auto;\n padding-block: 4px;\n pointer-events: auto;\n scrollbar-width: none;\n color: CanvasText;\n}\n.dsh-tl-scroll::-webkit-scrollbar {\n display: none;\n}\n.dsh-tl-track {\n position: relative;\n width: 36px;\n}\n.dsh-tl-slot {\n position: absolute;\n left: 0;\n top: 0;\n height: 10px;\n width: 36px;\n padding: 0;\n border: 0;\n background: transparent;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n border-radius: 2px;\n cursor: pointer;\n}\n.dsh-tl-slot:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n}\n.dsh-tl-tick {\n display: block;\n height: 2px;\n width: 12px;\n border-radius: 999px;\n background: currentColor;\n transform-origin: left center;\n transition: height 150ms ease-out, opacity 150ms ease-out, transform 150ms ease-out, background-color 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .dsh-tl-nav,\n .dsh-tl-tick {\n transition: none;\n }\n}\n.dsh-tl-tip {\n position: fixed;\n z-index: 60;\n width: 320px;\n max-width: calc(100vw - 2rem);\n padding: 12px;\n border-radius: 10px;\n background: Canvas;\n color: CanvasText;\n border: 1px solid color-mix(in srgb, CanvasText 15%, transparent);\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);\n pointer-events: none;\n}\n.dsh-tl-tip-user {\n margin: 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n}\n.dsh-tl-tip-assistant {\n margin: 8px 0 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n opacity: 0.8;\n}\n.dsh-tl-tip-assistant[data-kind=\"running\"],\n.dsh-tl-tip-assistant[data-kind=\"empty\"] {\n opacity: 0.55;\n}\n";
8
+ export declare const TIMELINE_STYLES = "\n.dsh-tl-nav {\n position: fixed;\n z-index: 10;\n width: 48px;\n /* \u6C34\u5E73\uFF1A\u4F1A\u8BDD\u5217\u5DE6\u6CBF\uFF0C\u7531 rail \u5199\u5165 --dsh-tl-left\u3002\u8FD9\u662F\u552F\u4E00\u4FDD\u7559\u7684\u6D4B\u91CF\u503C\u2014\u2014\n \u5217\u662F grid \u8F68\u9053\uFF0C\u53F3\u4FA7\u680F\u6253\u5F00\u65F6\u4F1A\u88AB\u6324\u538B\uFF0C\u4E0D\u8D34\u7A97\u53E3\u53F3\u6CBF\uFF0C\u65E0\u6CD5\u7531\u7A97\u53E3\u5BBD\u5EA6\u63A8\u5BFC\u3002 */\n left: var(--dsh-tl-left, 0px);\n /* \u5782\u76F4\uFF1A\u539F\u751F TurnNavigator \u7684 band \u6A21\u578B\uFF08eGxaPq_frame \u7684 --turn-rail-band\uFF09\u3002\n \u4F1A\u8BDD\u53EF\u89C6\u5E26 = \u89C6\u53E3\u9AD8 \u2212 \u8F93\u5165\u6846\u9AD8\uFF1B\u5176\u4E2D\u70B9\u5728\u89C6\u53E3\u5750\u6807\u4E0B\u4E3A\n 100dvh \u2212 (\u4F1A\u8BDD\u53EF\u89C6\u9AD8 + \u8F93\u5165\u6846\u9AD8) / 2\n \u4E24\u4E2A\u53D8\u91CF\u7531\u5BBF\u4E3B\u5199\u5728\u6EDA\u52A8\u5BB9\u5668\u4E0A\uFF0C\u672C seat \u5728\u6EDA\u52A8\u5BB9\u5668\u5185\u53EF\u76F4\u63A5\u7EE7\u627F\uFF0C\u6545\u6574\u4E2A\u5782\u76F4\n \u51E0\u4F55\u662F\u7EAF CSS\uFF1A\u4E0D\u505A\u6D4B\u91CF\uFF0C\u4E5F\u4E0D\u4F9D\u8D56\u539F\u751F nav \u662F\u5426\u5B58\u5728\u4E8E DOM\u3002 */\n --dsh-tl-band: calc(var(--dsh-conversation-viewport-height, 100dvh) - var(--dsh-composer-height, 152px));\n top: calc(100dvh - (var(--dsh-conversation-viewport-height, 100dvh) + var(--dsh-composer-height, 152px)) / 2);\n transform: translateY(-50%);\n pointer-events: none;\n opacity: 1;\n transition: opacity 150ms ease-out;\n}\n.dsh-tl-nav[data-visible=\"false\"] {\n opacity: 0;\n visibility: hidden;\n}\n.dsh-tl-scroll {\n position: relative;\n left: 12px;\n width: 36px;\n /* \u539F\u751F frame \u7684\u9AD8\u5EA6\u4E0A\u9650\uFF1Aband \u2212 64px\uFF0C\u518D\u5939\u5230 420px\u3002 */\n max-height: min(max(0px, calc(var(--dsh-tl-band) - 64px)), 420px);\n overflow-x: hidden;\n overflow-y: auto;\n padding-block: 4px;\n pointer-events: auto;\n scrollbar-width: none;\n color: CanvasText;\n}\n.dsh-tl-scroll::-webkit-scrollbar {\n display: none;\n}\n.dsh-tl-track {\n position: relative;\n width: 36px;\n}\n.dsh-tl-slot {\n position: absolute;\n left: 0;\n top: 0;\n height: 10px;\n width: 36px;\n padding: 0;\n border: 0;\n background: transparent;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n border-radius: 2px;\n cursor: pointer;\n}\n.dsh-tl-slot:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n}\n.dsh-tl-tick {\n display: block;\n height: 2px;\n width: 12px;\n border-radius: 999px;\n background: currentColor;\n transform-origin: left center;\n transition: height 150ms ease-out, opacity 150ms ease-out, transform 150ms ease-out, background-color 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .dsh-tl-nav,\n .dsh-tl-tick {\n transition: none;\n }\n}\n.dsh-tl-tip {\n position: fixed;\n z-index: 60;\n width: 320px;\n max-width: calc(100vw - 2rem);\n padding: 12px;\n border-radius: 10px;\n /* \u539F\u751F tooltip \u914D\u5BF9\uFF08dsh-web-frontend _bubble_\uFF09\uFF1A\u7070\u5E95 + \u9759\u6001\u767D\u5B57\uFF0C\n \u4E3B\u9898\u5207\u6362\u7531\u522B\u540D\u53D8\u91CF\u8DDF\u968F\uFF1B\u7070\u5E95\u4E0E\u9875\u9762\u80CC\u666F\u5929\u7136\u5206\u5C42\u3002 */\n background: var(--dsw-alias-tooltip-bg, #43454a);\n color: var(--dsw-static-neutral-bluish-00, #fff);\n border: none;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);\n pointer-events: none;\n}\n.dsh-tl-tip-user {\n margin: 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n}\n.dsh-tl-tip-assistant {\n margin: 8px 0 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-static-neutral-bluish-300, rgba(255, 255, 255, 0.8));\n}\n.dsh-tl-tip-assistant[data-kind=\"running\"],\n.dsh-tl-tip-assistant[data-kind=\"empty\"] {\n color: var(--dsw-static-neutral-bluish-400, rgba(255, 255, 255, 0.55));\n}\n/* \u63D2\u4EF6\u5B8C\u5168\u63A5\u7BA1\u539F\u751F\u53F3\u4FA7 Turn \u5BFC\u822A\u6761\uFF1A\u672C\u6837\u5F0F\u8868\u53EA\u968F\u63D2\u4EF6\u5B58\u5728\uFF0C\u63D2\u4EF6\u5378\u8F7D\u65F6\n removeStyles \u79FB\u9664\u6807\u7B7E\uFF0C\u539F\u751F\u5BFC\u822A\u6761\u7ACB\u5373\u6062\u590D\u3002aria-label \u8DDF\u968F\u5BBF\u4E3B\u8BED\u8A00\n \uFF08\u82F1\u6587 Turn navigation / \u4E2D\u6587 \u8F6E\u6B21\u5BFC\u822A\uFF09\uFF0C\u4E24\u4E2A\u90FD\u8981\u76D6\u4F4F\u3002 */\nnav[aria-label='Turn navigation'],\nnav[aria-label='\u8F6E\u6B21\u5BFC\u822A'] {\n display: none !important;\n}\n";
9
9
  /** Inject the stylesheet once per document. */
10
10
  export declare function ensureStyles(doc: Document): void;
11
+ /** Remove the stylesheet (plugin teardown): restores the native turn navigator. */
12
+ export declare function removeStyles(doc: Document): void;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@lament_z/dsh-client-ui-chat-timeline",
3
3
  "displayName": "DSH Chat Timeline",
4
4
  "description": "DSH Web 对话时间线插件:在左侧提供仿 ZCode TurnNavigator 的提问导航条,每个用户问题一个刻度,支持悬停波纹反馈、轮次预览卡片、点击跳转定位与滚动同步高亮,快速回溯长对话。",
5
- "version": "0.1.0",
5
+ "version": "0.2.0",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
8
8
  "types": "lib/types/index.d.ts",
@@ -26,9 +26,6 @@
26
26
  "patch": "./cordis.patch.yml"
27
27
  },
28
28
  "client": {
29
- "inject": [
30
- "@deepseek-ai/dsh-client-runtime"
31
- ],
32
29
  "platform": "web"
33
30
  }
34
31
  },
@@ -42,11 +39,13 @@
42
39
  "react": "^18.2.0"
43
40
  },
44
41
  "devDependencies": {
45
- "@deepseek-ai/cordis": "^4.0.1",
46
- "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
47
- "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
48
- "@deepseek-ai/dsh-client-ui-layout": "0.1.1-rc.2",
49
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
42
+ "@deepseek-ai/cordis": "^4.0.2",
43
+ "@deepseek-ai/dsh-api-session-controller": "0.1.7-rc.2",
44
+ "@deepseek-ai/dsh-client-locale": "0.1.7-rc.2",
45
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2",
46
+ "@deepseek-ai/dsh-client-ui-layout": "0.1.7-rc.2",
47
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.2",
48
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.2",
50
49
  "@types/react": "~18.3.1",
51
50
  "@types/react-dom": "^18.3.5",
52
51
  "jsdom": "29.1.1",