@lament_z/dsh-client-ui-chat-timeline 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js.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 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 /* 原生 TurnNavigator 定位(eGxaPq_frame):中心 = 内容顶 + (视口高 − 输入框高)/2,\n 即 --turn-rail-band 的中点;centerY 由 rail 按同一公式写入。 */\n top: var(--dsh-tl-center-y, 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 /* 原生 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 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 { 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 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/** 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 /** Rail band center, viewport-relative to the container top (native --turn-rail-band rule). */\n centerY: number\n}\n\n/** Default composer height when the host variable is absent (native fallback). */\nconst COMPOSER_HEIGHT_FALLBACK = 152\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({ 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 [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 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 // 0.1.5 原生锚点:行按 `data-chat-turn=\"<turn>\"` 键控(dsh-client-ui-chat\n // ChatView),跳转与高亮都按轮次号走原生逻辑;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 // 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 // 原生 TurnNavigator(eGxaPq_frame)定位复刻:\n // 中心 = sticky 槽位 y + (视口高 − 输入框高) / 2,两个高度都读宿主变量。\n // 槽位即原生导航条的父元素(sticky, height 0),拿不到时退化为容器顶。\n const style = getComputedStyle(container)\n const composerHeight = Number.parseFloat(style.getPropertyValue('--dsh-composer-height')) || COMPOSER_HEIGHT_FALLBACK\n const viewportHeight = Number.parseFloat(style.getPropertyValue('--dsh-conversation-viewport-height')) || window.innerHeight\n const slot = document.querySelector('nav[aria-label=\"Turn navigation\"]')?.parentElement\n const slotTop = slot !== null && slot !== undefined ? slot.getBoundingClientRect().top : box.top\n const bandCenter = (viewportHeight - composerHeight) / 2\n setRect({\n left: box.left,\n top: box.top,\n height: box.height,\n width: box.width,\n centerY: Math.max(0, slotTop - box.top + bandCenter),\n })\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 }, [probe, containerVersion])\n\n // items 变化(事件窗口增长/切换会话)后重算一次 active 高亮。\n useEffect(() => {\n if (probe === null || containerVersion === 0 || rect === null) return\n setActiveIndex(probe.activeIndex(turns))\n }, [probe, containerVersion, rect, turns])\n\n // 完全模仿原生:只要会话可读且容器在,刻度条就渲染——不设条数/宽度门槛,\n // 原生导航条的隐藏完全交给本插件的接管规则。\n const visible = snapshot !== null && rect !== 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 || 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, '--dsh-tl-center-y': `${rect.centerY}px` } as React.CSSProperties}\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.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.5: `binding.session.projections.faceOf('turnOutline')` is the official\n * whole-log turn index; `loadThrough(seq)` is the turn-jump pagination verb.\n */\nexport interface SessionsLike {\n list: ObservableLike<{ current?: string }>\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\nconst EMPTY_STATE: TimelineState = { sessionId: undefined, snapshot: null }\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/**\n * Create the source. Subscribes to the sessions list, rebinds to the current\n * session's outline projection on selection change (polling while no session\n * is selected — the list store need not notify on pure selection switches),\n * and republishes a stable state whenever the outline or running flag moves.\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 subscribed = false\n let unbindSession: (() => void) | null = null\n let boundSessionId: string | undefined\n let retryTimer: ReturnType<typeof setTimeout> | undefined\n let retryAttempts = 0\n let noSessionPolls = 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 === undefined) {\n // 尚未选中会话:list 存储在纯选择切换时可能不通知,轮询等它出现。\n state = EMPTY_STATE\n emit()\n if (subscribed && noSessionPolls < 60) {\n noSessionPolls += 1\n retryTimer = setTimeout(rebind, 1000)\n }\n return\n }\n noSessionPolls = 0\n boundSessionId = current\n unbindSession?.()\n unbindSession = null\n let bound: ReturnType<NonNullable<SessionsLike['binding']>> | undefined\n try {\n bound = sessions.binding(current)\n } catch {\n bound = undefined\n }\n const session = bound?.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 // 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 outline = outlineFace.getSnapshot()\n const running = Boolean(session.getSnapshot()?.running)\n state = {\n sessionId: current,\n snapshot: {\n sessionId: current,\n items: Array.isArray(outline)\n ? itemsFromOutline(outline, running, {\n userFallback: '(无文本)',\n assistantRunning: '(运行中)',\n assistantEmpty: '(空)',\n })\n : [],\n running,\n },\n }\n } catch {\n state = { sessionId: current, snapshot: null }\n }\n emit()\n }\n push()\n try {\n outlineFace.subscribe(push)\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 subscribed = true\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 jumpThrough(seq: number): Promise<void> {\n let current: string | undefined\n try {\n current = sessions.list.getSnapshot()?.current\n } catch {\n current = undefined\n }\n const session = current === undefined ? undefined : sessions.binding(current)?.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. */\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 */\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// 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/** 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 // console 错误(不抛进 loader,也不静默——留诊断线索)。\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 (err) {\n console.error('chat-timeline: shell.overlay 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,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,61 @@ 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.5: `binding.session.projections.faceOf('turnOutline')` is the official
45
+ * whole-log turn index; `loadThrough(seq)` is the turn-jump pagination verb.
46
+ */
32
47
  export interface SessionsLike {
33
- readonly list: ObservableLike<{
48
+ list: ObservableLike<{
34
49
  current?: string;
35
50
  }>;
36
51
  binding(id: string): {
37
52
  session: ObservableLike<{
38
- nodes?: readonly unknown[];
39
53
  running?: boolean;
40
- }>;
54
+ }> & {
55
+ loadThrough?(seq: number): Promise<void>;
56
+ projections?: {
57
+ faceOf?(key: string): ObservableSnapshotLike<unknown>;
58
+ };
59
+ };
60
+ eventSource: ObservableLike<unknown>;
41
61
  } | undefined;
42
62
  }
63
+ /** ObservableSnapshot 的最小结构面(getSnapshot/subscribe)。 */
64
+ type ObservableSnapshotLike<T> = ObservableLike<T>;
65
+ /** Project one outline entry onto a tick (previews are host-bounded already). */
66
+ export declare function itemFromOutline(entry: TurnOutlineEntryLike, isLive: boolean, labels: {
67
+ userFallback: string;
68
+ assistantRunning: string;
69
+ assistantEmpty: string;
70
+ }): TimelineItem;
71
+ /** Build the tick list from the outline plus the live running flag. */
72
+ export declare function itemsFromOutline(outline: readonly TurnOutlineEntryLike[], running: boolean, labels: {
73
+ userFallback: string;
74
+ assistantRunning: string;
75
+ assistantEmpty: string;
76
+ }): TimelineItem[];
77
+ export { buildPreview, MAX_PREVIEW_CHARS, MAX_PREVIEW_PARAGRAPHS };
43
78
  /**
44
79
  * 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.
80
+ * session's outline projection on selection change (polling while no session
81
+ * is selected — the list store need not notify on pure selection switches),
82
+ * and republishes a stable state whenever the outline or running flag moves.
47
83
  * @param sessions - the sessions service face (pass ctx.sessions).
48
84
  * @returns the source face.
49
85
  */
50
86
  export declare function createTimelineSource(sessions: SessionsLike): TimelineSource;
51
87
  /** Build the source from the plugin client context. */
52
88
  export declare function timelineSourceFromContext(ctx: ClientContext): TimelineSource;
53
- export {};
@@ -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 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 /* \u539F\u751F TurnNavigator \u5B9A\u4F4D\uFF08eGxaPq_frame\uFF09\uFF1A\u4E2D\u5FC3 = \u5185\u5BB9\u9876 + (\u89C6\u53E3\u9AD8 \u2212 \u8F93\u5165\u6846\u9AD8)/2\uFF0C\n \u5373 --turn-rail-band \u7684\u4E2D\u70B9\uFF1BcenterY \u7531 rail \u6309\u540C\u4E00\u516C\u5F0F\u5199\u5165\u3002 */\n top: var(--dsh-tl-center-y, 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 /* \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.1.1",
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,10 @@
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-client-locale": "0.1.5-rc.2",
44
+ "@deepseek-ai/dsh-client-ui-layout": "0.1.5-rc.2",
45
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
50
46
  "@types/react": "~18.3.1",
51
47
  "@types/react-dom": "^18.3.5",
52
48
  "jsdom": "29.1.1",
@@ -54,7 +50,9 @@
54
50
  "react-dom": "^18.3.1",
55
51
  "tsdown": "0.22.2",
56
52
  "typescript": "~5.7.2",
57
- "vitest": "^4.1.8"
53
+ "vitest": "^4.1.8",
54
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.2",
55
+ "@deepseek-ai/dsh-api-session-controller": "0.1.5-rc.2"
58
56
  },
59
57
  "files": [
60
58
  "lib",