@khorsheed/dsh-message-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":["useMemo","useState","useRef","createPortal","css","memo"],"sources":["../src/client/config.ts","../src/client/locales.ts","../src/client/rail-tracker.ts","../src/client/preview.ts","../src/client/TimelineRail.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * Browser-half configuration of the message-timeline plugin. The runner hands\n * the plugin its validated entry config through the apply second parameter;\n * this module supplies the defaults and clamps, so a composition that does not\n * pass config still renders with the documented values and out-of-range input\n * lands inside the legal bounds.\n */\n\n/** Deployment-tunable rail behavior. */\nexport interface TimelineConfig {\n /** Master switch: false hides the toggle and the rail entirely. */\n enabled: boolean\n /** Count steering messages (user text admitted mid-turn) as rows. */\n includeSteering: boolean\n /**\n * Preferred timeline panel width in px (clamped 120–640; long text\n * ellipsizes). The panel's right edge never crosses the message flow: the\n * width is capped by the scrollport's left gutter, so a narrow column\n * shrinks the panel automatically, and a gutter too small for\n * {@link PANEL_WIDTH_MIN} hides the panel entirely.\n */\n panelWidth: number\n /**\n * History pages to prefetch when the rail opens (50 events each); older\n * pages load on demand when the rail or the panel is scrolled to its top.\n */\n initialPages: number\n}\n\n/** Bounds of the deployment-tunable numbers (clamped in resolveConfig). */\nexport const PANEL_WIDTH_MIN = 120\nexport const PANEL_WIDTH_MAX = 640\nconst INITIAL_PAGES_MIN = 1\nconst INITIAL_PAGES_MAX = 20\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\n/**\n * Normalize the entry config into the full {@link TimelineConfig}: every\n * omitted field takes its documented default and every numeric field is\n * clamped into its legal range.\n * @param config - the unvalidated entry config, when the runner passes one.\n * @returns the effective rail behavior.\n */\nexport function resolveConfig(config: Partial<TimelineConfig> | undefined): TimelineConfig {\n return {\n enabled: config?.enabled ?? true,\n includeSteering: config?.includeSteering ?? true,\n panelWidth: clamp(config?.panelWidth ?? 360, PANEL_WIDTH_MIN, PANEL_WIDTH_MAX),\n initialPages: clamp(config?.initialPages ?? 5, INITIAL_PAGES_MIN, INITIAL_PAGES_MAX),\n }\n}\n","/** `message-timeline` namespace dictionaries. */\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'rail.panel': '消息导览',\n 'rail.empty': '暂无用户消息',\n} satisfies Record<string, string>\n\n/** The message-timeline namespace key union. */\nexport type TimelineKey = keyof typeof zh\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The message timeline panel's copy. */\n 'message-timeline': TimelineKey\n }\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en = {\n 'rail.panel': 'Message timeline',\n 'rail.empty': 'No user messages',\n} satisfies Record<TimelineKey, string>\n","/**\n * Rail DOM tracker, apply world. The floating timeline panel needs the\n * official chat scrollport's live geometry and the currently visible\n * user-message row, so\n * this module owns the only DOM the plugin touches — read-only probes plus\n * the scroll write the jump performs — and publishes the result through the\n * reserved hooks compartment (components never see the DOM or the sources).\n *\n * The probed attributes ([data-conversation-scroll], [data-chat-anchor-key],\n * [data-chat-flow-kind]) are official render output, not a declared API: when\n * they change, the tracker degrades — a missing scrollport hides the rail\n * (one console.warn), missing rows only clear the active marker and make\n * jumps no-ops. Nothing throws and no official code is modified.\n */\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { TimelineRailState } from './slots.ts'\n\n/** Horizontal inset of the rail from the scrollport's left edge (px). */\nconst RAIL_LEFT_INSET = 6\n/** Vertical padding above and below the rail inside the scrollport (px). */\nconst RAIL_VERTICAL_PADDING = 8\n/** Keep the target row this far below the scrollport top after a jump (px). */\nconst JUMP_OFFSET = 16\n\n/** Idle state published before any session binds or while none is current. */\nconst IDLE: TimelineRailState = {\n sessionId: undefined, ready: false, left: 0, top: 0, height: 0, scrollportWidth: 0, flowLeft: null,\n activeKey: null, chatView: false,\n}\n\n/**\n * Measure the panel's viewport box from one scrollport: its rect inset by the\n * panel padding, minus the sticky composer seat at the bottom and the\n * conversation tab strip at the top. The tabs render just above the\n * scrollport, but centering reads against the whole window, so the strip\n * height leaves the box either way — otherwise the list sits visibly high.\n * The scrollport's own width rides along for the width-cap fallback when the\n * message-flow probe is unanswered.\n * @param scrollport - the official conversation scrollport element.\n * @returns the panel box plus the scrollport width, or null while the\n * scrollport has no laid-out size.\n */\nexport function measureGeometry(scrollport: HTMLElement): { left: number; top: number; height: number; width: number } | null {\n const rect = scrollport.getBoundingClientRect()\n if (rect.width === 0 && rect.height === 0) return null\n const composer = scrollport.querySelector<HTMLElement>('[data-composer-seat]')\n const composerHeight = composer?.getBoundingClientRect().height ?? 0\n let topInset = RAIL_VERTICAL_PADDING\n for (const tabs of scrollport.ownerDocument.querySelectorAll<HTMLElement>('[role=\"tablist\"]')) {\n const tabsRect = tabs.getBoundingClientRect()\n // Only the strip adjacent to the scrollport's top edge counts as chrome.\n if (tabsRect.height > 0 && Math.abs(tabsRect.bottom - rect.top) <= 80) {\n topInset += tabsRect.height\n break\n }\n }\n return {\n left: rect.left + RAIL_LEFT_INSET,\n top: rect.top + topInset,\n height: Math.max(0, rect.height - composerHeight - topInset - RAIL_VERTICAL_PADDING),\n width: rect.width,\n }\n}\n\n/**\n * The viewport x of the message flow's left edge: the left of the first\n * rendered `[data-chat-flow-kind]` row, which sits flush inside the official\n * centered content column (max 748px, `margin: 0 auto`). Every flow row\n * shares that edge, so the first one found suffices. The panel's right edge\n * stays left of it — the panel may only occupy the scrollport's left gutter.\n * @param scrollport - the official conversation scrollport element.\n * @returns the flow's left edge, or null while no flow row is rendered.\n */\nexport function flowLeftX(scrollport: HTMLElement): number | null {\n const row = scrollport.querySelector<HTMLElement>('[data-chat-flow-kind]')\n return row === null ? null : row.getBoundingClientRect().left\n}\n\n/**\n * Resolve the key of the user-message row the reading position belongs to:\n * the first matching row whose bottom is still inside the viewport, or —\n * while the reader sits inside a long assistant answer with no user row\n * visible — the nearest user row above the viewport, so the lit tick stays\n * anchored to the question being answered instead of jumping to the\n * session's latest message.\n * @param scrollport - the official conversation scrollport element.\n * @param includeSteering - whether steering rows count as user messages.\n * @returns the row's anchor key, or null when no user row is rendered.\n */\nexport function activeRowKey(scrollport: HTMLElement, includeSteering: boolean): string | null {\n const viewTop = scrollport.getBoundingClientRect().top\n let lastAbove: string | null = null\n for (const row of scrollport.querySelectorAll<HTMLElement>('[data-chat-flow-kind]')) {\n const kind = row.dataset.chatFlowKind\n if (kind !== 'user' && !(includeSteering && kind === 'steering')) continue\n if (row.getBoundingClientRect().bottom <= viewTop) {\n lastAbove = row.dataset.chatAnchorKey ?? lastAbove\n continue\n }\n return row.dataset.chatAnchorKey ?? null\n }\n return lastAbove\n}\n\n/**\n * Scroll one user-message row to the top of the transcript scrollport.\n * @param scrollport - the official conversation scrollport element.\n * @param key - the target node's anchor key.\n * @returns whether the row was found and scrolled.\n */\nexport function jumpRow(scrollport: HTMLElement, key: string): boolean {\n const row = scrollport.querySelector<HTMLElement>(`[data-chat-anchor-key=${JSON.stringify(key)}]`)\n if (row === null) return false\n const target = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top\n + scrollport.scrollTop - JUMP_OFFSET\n scrollport.scrollTop = Math.max(0, target)\n return true\n}\n\n/** The tracker's outward face: the observable state, the jump verb, and the disposer. */\nexport interface RailTracker {\n /** Live rail geometry and active marker for the current session. */\n readonly state: HostObservable<TimelineRailState>\n /** Scroll the transcript to the message addressed by `key` (no-op while unbound). */\n jumpTo(key: string): void\n /** Unbind every listener and observer. */\n dispose(): void\n}\n\n/**\n * Install the rail tracker: bind the current session's scrollport, publish\n * geometry/active updates on scroll and resize, and answer jump requests.\n * Follows the current session through the sessions list and provide channels\n * (the interface renders one conversation at a time, so one tracker suffices).\n * @param ctx - client root context (sessions service).\n * @param includeSteering - whether steering rows count as user dots.\n * @returns the tracker face.\n */\nexport function installRailTracker(ctx: ClientContext, includeSteering: boolean): RailTracker {\n const listeners = new Set<() => void>()\n let state: TimelineRailState = IDLE\n let activeSession: SessionId | undefined\n let scrollport: HTMLElement | null = null\n let resizeObserver: ResizeObserver | undefined\n let rafPending = false\n let warned = false\n\n const nextFrame = typeof requestAnimationFrame === 'function'\n ? requestAnimationFrame\n : (callback: () => void): void => { setTimeout(callback, 16) }\n\n const same = (left: TimelineRailState, right: TimelineRailState): boolean =>\n left.sessionId === right.sessionId && left.ready === right.ready\n && left.left === right.left && left.top === right.top\n && left.height === right.height && left.scrollportWidth === right.scrollportWidth\n && left.flowLeft === right.flowLeft\n && left.activeKey === right.activeKey\n && left.chatView === right.chatView\n\n const publish = (next: TimelineRailState): void => {\n if (same(state, next)) return\n state = next\n for (const fn of [...listeners]) fn()\n }\n\n const update = (): void => {\n if (scrollport === null) return\n const geometry = measureGeometry(scrollport)\n if (geometry === null) return\n publish({\n sessionId: activeSession,\n ready: true,\n left: geometry.left,\n top: geometry.top,\n height: geometry.height,\n scrollportWidth: geometry.width,\n flowLeft: flowLeftX(scrollport),\n activeKey: activeRowKey(scrollport, includeSteering),\n chatView: scrollport.querySelector('[data-chat-flow]') !== null,\n })\n }\n\n const scheduleUpdate = (): void => {\n if (rafPending) return\n rafPending = true\n nextFrame(() => {\n rafPending = false\n update()\n })\n }\n\n const onScroll = (): void => { scheduleUpdate() }\n\n const teardownBind = (): void => {\n if (scrollport !== null) scrollport.removeEventListener('scroll', onScroll)\n scrollport = null\n resizeObserver?.disconnect()\n resizeObserver = undefined\n rafPending = false\n }\n\n const bind = (sessionId: SessionId | undefined): void => {\n if (sessionId === activeSession && scrollport !== null) return\n teardownBind()\n activeSession = sessionId\n if (sessionId === undefined) {\n publish(IDLE)\n return\n }\n nextFrame(() => {\n const found = document.querySelector<HTMLElement>('[data-conversation-scroll]')\n if (found === null) {\n if (!warned) {\n warned = true\n console.warn('message-timeline: [data-conversation-scroll] not found; the rail stays hidden')\n }\n publish(IDLE)\n return\n }\n scrollport = found\n scrollport.addEventListener('scroll', onScroll, { passive: true })\n if (typeof ResizeObserver === 'function') {\n resizeObserver = new ResizeObserver(scheduleUpdate)\n resizeObserver.observe(scrollport)\n const composer = scrollport.querySelector<HTMLElement>('[data-composer-seat]')\n if (composer !== null) resizeObserver.observe(composer)\n }\n update()\n })\n }\n\n const bindCurrent = (): void => {\n bind(ctx.sessions.list.getSnapshot().current)\n }\n const stopList = ctx.sessions.list.subscribe(bindCurrent)\n const stopProvide = ctx.sessions.currentProvideInfo.subscribe(bindCurrent)\n bindCurrent()\n\n // Layout fallbacks beyond the scrollport's own ResizeObserver: a window\n // resize re-measures, and a body MutationObserver catches panel folds and\n // other layout changes that resize the scrollport without a window event.\n // Both go through the rAF-throttled scheduleUpdate, and publish() skips\n // identical geometry, so the cost stays one measurement per changed frame.\n const onWindowResize = (): void => { scheduleUpdate() }\n if (typeof window !== 'undefined') window.addEventListener('resize', onWindowResize)\n let mutationObserver: MutationObserver | undefined\n if (typeof MutationObserver === 'function' && typeof document !== 'undefined') {\n mutationObserver = new MutationObserver(scheduleUpdate)\n mutationObserver.observe(document.body, { childList: true, subtree: true })\n }\n\n return {\n state: {\n getSnapshot: () => state,\n subscribe: (fn) => {\n listeners.add(fn)\n return () => { listeners.delete(fn) }\n },\n },\n jumpTo: (key) => {\n if (scrollport !== null) jumpRow(scrollport, key)\n },\n dispose: () => {\n stopList()\n stopProvide()\n if (typeof window !== 'undefined') window.removeEventListener('resize', onWindowResize)\n mutationObserver?.disconnect()\n teardownBind()\n },\n }\n}\n","/**\n * Pure text extraction for the timeline panel rows: user message content is a\n * block list (text, image, …); the preview concatenates the text blocks so\n * long messages ellipsize through CSS instead of dropping their first lines.\n */\nimport type { ContentBlock } from '@deepseek-ai/dsh-llm'\n\n/**\n * Join the text blocks of one message into a single preview string.\n * @param content - the message's content block list.\n * @returns the concatenated text, or null when the message carries none.\n */\nexport function previewText(content: readonly ContentBlock[]): string | null {\n const parts: string[] = []\n for (const block of content) {\n if (block.type === 'text' && block.text !== '') parts.push(block.text)\n }\n return parts.length === 0 ? null : parts.join('\\n')\n}\n","/**\n * Message timeline panel, browser half. One entry in the official\n * conversation.session.header.utilities seat anchors the plugin into the\n * session scope and renders, through a body portal, the flat floating\n * timeline over the left edge of the chat scrollport: one row per loaded\n * user message — a tick plus an ellipsized one-line preview, no frame and no\n * visible scrollbar. At rest only the dimmed ticks show, the reading\n * position's tick in blue (the latest message until the tracker answers);\n * hovering or keyboard-focusing the panel reveals the row texts with the\n * blue row on top, and clicking a row jumps the transcript to that message.\n * The panel is always on while the chat view shows; the `enabled` config is\n * the off switch.\n */\nimport { memo, useEffect, useMemo, useRef, useState, type UIEvent } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { ContentBlock } from '@deepseek-ai/dsh-llm'\nimport { PANEL_WIDTH_MIN } from './config.ts'\nimport type { TimelineItem, TimelineRailProps } from './slots.ts'\nimport { previewText } from './preview.ts'\nimport css from './TimelineRail.module.css'\n\n/** Extract the preview blocks of one user/steering node (kind-checked by the caller). */\nfunction nodeContent(node: { data: unknown }): readonly ContentBlock[] {\n return (node.data as { content?: readonly ContentBlock[] }).content ?? []\n}\n\n/**\n * Horizontal panel padding, matching `.panel`'s `padding: 4px 8px`: the text\n * band sits this far inside the box, so the width budget must give it back\n * for {@link PANEL_GAP} to be the visible gap to the message flow.\n */\nconst PANEL_PADDING_X = 8\n/** Visible breathing gap between the timeline text and the message flow (px). */\nconst PANEL_GAP = 16\n/** Degraded width cap (fraction of the scrollport) while the flow probe is unanswered. */\nconst DEGRADED_WIDTH_RATIO = 0.4\n\n/**\n * The header-utilities entry: the portal timeline panel.\n * @param props - composed props (see {@link TimelineRailProps}).\n * @returns nothing visible in the seat; the floating panel while the chat view shows.\n */\nexport function TimelineRail({\n useSession, sessionId,\n includeSteering, panelWidth, initialPages,\n loadOlder, jumpTo, useRail, t,\n}: TimelineRailProps) {\n const rail = useRail(s => s)\n const order = useSession(s => s.chat.order)\n const nodes = useSession(s => s.chat.nodes)\n const hasMore = useSession(s => s.hasMore)\n const loadingOlder = useSession(s => s.loadingOlder)\n\n const items = useMemo<TimelineItem[]>(() => {\n const result: TimelineItem[] = []\n for (const key of order) {\n const node = nodes.get(key)\n if (node === undefined) continue\n const kind = node.kind\n if (kind !== 'user' && !(includeSteering && kind === 'steering')) continue\n result.push({ key, node })\n }\n return result\n }, [order, nodes, includeSteering])\n\n // The lit row: the hover/arrow preselection while it moves, otherwise the\n // live reading position the tracker publishes, defaulting to the latest\n // message before the tracker answers. ArrowUp/Down or hovering moves a\n // bold preselection; Enter or a click confirms it and jumps.\n const [focusKey, setFocusKey] = useState<string | null>(null)\n const current = focusKey ?? rail.activeKey ?? items.at(-1)?.key ?? null\n const panelRef = useRef<HTMLDivElement | null>(null)\n\n // The panel is a chat-view affordance: hide it while the session shows\n // another tab (trajectory etc.), detected through ChatView's data-chat-flow\n // marker. `active` splits from `visible` so history paging can bootstrap: a\n // session whose loaded event window holds no user message yet (a huge\n // assistant turn pushed it past the first page) renders no rows but must\n // still pull pages until one materializes.\n const active = rail.chatView && rail.ready && rail.sessionId === sessionId\n const visible = active && items.length > 0\n\n // The panel must never cover the message flow: its width is the configured\n // preferred width capped by the scrollport's left gutter — the message\n // flow's left edge minus the panel's left edge, less the panel's right\n // padding and the visible breathing gap. A gutter too small for the minimum\n // usable width hides the panel entirely (the timeline is an overlay\n // affordance; squeezed into nothing it only intercepts the transcript).\n // When the flow probe is unanswered (official structure change), the width\n // degrades to a fraction of the scrollport instead — never throws, never\n // covers more than the fallback.\n const gutter = rail.flowLeft === null ? null : rail.flowLeft - rail.left - PANEL_PADDING_X - PANEL_GAP\n const width = gutter === null\n ? Math.min(panelWidth, Math.max(PANEL_WIDTH_MIN, rail.scrollportWidth * DEGRADED_WIDTH_RATIO))\n : Math.min(panelWidth, Math.max(0, gutter))\n const tooNarrow = gutter !== null && width < PANEL_WIDTH_MIN\n\n // Keep the lit row in view: the panel follows the reading position (a new\n // message scrolls its row in), and mouse browsing is never yanked because\n // the hovered row is the current one and always visible under the pointer.\n useEffect(() => {\n panelRef.current?.querySelector<HTMLElement>(`[data-item-key=${JSON.stringify(current)}]`)\n ?.scrollIntoView({ block: 'nearest' })\n }, [current])\n\n // Prefetch history while the panel is on a chat view: keep pulling pages\n // until the first user message materializes (bootstrap), then until\n // initialPages pages arrived, so the list starts near-complete. Older\n // pages load on demand when the panel scrolls to its top.\n const prefetchedPagesRef = useRef(0)\n useEffect(() => {\n if (!active) {\n prefetchedPagesRef.current = 0\n return\n }\n if (!hasMore || loadingOlder) return\n if (items.length > 0 && prefetchedPagesRef.current >= initialPages) return\n prefetchedPagesRef.current += 1\n void loadOlder()\n }, [active, hasMore, loadingOlder, items.length, initialPages, loadOlder])\n\n // Older history loads by scrolling the panel to its top; the official chat\n // view owns the load-older button, so the panel stays chromeless.\n const onPanelScroll = (event: UIEvent<HTMLDivElement>): void => {\n if (!hasMore || loadingOlder) return\n if (event.currentTarget.scrollTop <= 8) void loadOlder()\n }\n\n const confirm = (key: string): void => {\n setFocusKey(null)\n jumpTo(key)\n }\n\n if (!visible || tooNarrow) return null\n return createPortal(\n <div\n ref={panelRef}\n className={css.panel}\n role=\"navigation\"\n aria-label={t('rail.panel')}\n tabIndex={0}\n data-timeline-panel=\"\"\n style={{ left: rail.left, top: rail.top, height: rail.height, width }}\n onScroll={onPanelScroll}\n onKeyDown={(event) => {\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault()\n const anchor = items.findIndex(item => item.key === current)\n const next = event.key === 'ArrowDown'\n ? items[Math.min(anchor + 1, items.length - 1)]\n : items[Math.max(anchor - 1, 0)]\n /* v8 ignore next -- a non-empty item list keeps the clamped index inside bounds */\n if (next !== undefined) setFocusKey(next.key)\n } else if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n /* v8 ignore next -- the panel only renders with at least one item, so a current key always exists */\n if (current !== null) confirm(current)\n }\n }}\n onBlur={() => { setFocusKey(null) }}\n >\n {items.map((item) => {\n const isCurrent = item.key === current\n const isFocused = item.key === focusKey\n const className = isFocused\n ? `${css.item} ${css.itemFocused}`\n : isCurrent ? `${css.item} ${css.itemCurrent}` : css.item\n return (\n <button\n key={item.key}\n type=\"button\"\n data-item-key={item.key}\n aria-current={isCurrent || undefined}\n className={className}\n onClick={() => { confirm(item.key) }}\n onMouseEnter={() => { setFocusKey(item.key) }}\n onMouseLeave={() => { setFocusKey(key => key === item.key ? null : key) }}\n >\n <span className={isCurrent ? `${css.tick} ${css.tickCurrent}` : css.tick} aria-hidden=\"true\" />\n <span className={css.itemText}>{previewText(nodeContent(item.node)) ?? t('rail.empty')}</span>\n </button>\n )\n })}\n </div>,\n document.body,\n )\n}\n\n/** Memoized export for the slot machinery (stable component identity). */\nexport const TimelineRailEntry = memo(TimelineRail)\n","/**\n * Message timeline plugin, browser half. Registers one entry into the\n * official `conversation.session.header.utilities` seat (a right-aligned\n * optional-utility slot) that anchors the plugin into the session scope; the\n * component itself renders only through a body portal: the flat floating\n * timeline panel over the chat scrollport's left edge — one row per loaded\n * user message, no frame, no visible scrollbar. The\n * panel reads the session chat snapshot through the framework `useSession`\n * hook, jumps through the official row anchor attributes, and degrades to a\n * hidden panel when those attributes change — no official code is modified.\n * @module @khorsheed/dsh-message-timeline/client\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: pulls the ctx.locale service merge.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls ui-conversation's SlotMap merge (the header utilities seat)\n// and the conversation service merge.\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { resolveConfig, type TimelineConfig } from './config.ts'\nimport { en, zh } from './locales.ts'\nimport { installRailTracker } from './rail-tracker.ts'\nimport type { TimelineRailInjected } from './slots.ts'\nimport { TimelineRailEntry } from './TimelineRail.tsx'\n\nexport type { TimelineConfig } from './config.ts'\nexport { resolveConfig } from './config.ts'\nexport type { TimelineKey } from './locales.ts'\nexport { previewText } from './preview.ts'\nexport type { TimelineItem, TimelineRailInjected, TimelineRailProps, TimelineRailState } from './slots.ts'\n\n/** Dictionary namespace owned by this plugin. */\nconst NS = 'message-timeline'\n\n/** Required services: the slot ledger, the session store, and the copy. */\nexport const inject = ['slots', 'sessions', 'locale']\n\n/**\n * Client plugin body: register the header-utilities entry and install the DOM\n * tracker that publishes rail geometry and answers jumps.\n * @param ctx - client root context.\n * @param config - entry config; defaults apply when the runner passes none.\n */\nexport function apply(ctx: ClientContext, config?: Partial<TimelineConfig>): void {\n const options = resolveConfig(config)\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'message-timeline: dictionaries')\n if (!options.enabled) return\n\n const tracker = installRailTracker(ctx, options.includeSteering)\n ctx.effect(() => () => { tracker.dispose() }, 'message-timeline: rail tracker')\n\n // The slot is declared by ui-conversation, whose apply order relative to\n // this plugin is unconstrained: register through slots.inject so the entry\n // waits for the declaration instead of crashing the loader at boot.\n ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({\n name: 'conversation.session.header.utilities',\n id: 'message-timeline',\n order: 100,\n locale: NS,\n inject: (sessionId): TimelineRailInjected => {\n const actx = ctx.sessions.scope(sessionId)\n if (actx === undefined) throw new Error('message-timeline: session resolved no scope')\n const conversation = actx.get('conversation')\n if (conversation === undefined) throw new Error('message-timeline: conversation service unavailable')\n return {\n includeSteering: options.includeSteering,\n panelWidth: options.panelWidth,\n initialPages: options.initialPages,\n loadOlder: () => conversation.loadOlder(),\n jumpTo: (key) => { tracker.jumpTo(key) },\n hooks: { rail: tracker.state },\n }\n },\n }, TimelineRailEntry))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useMemo","useState","useRef","createPortal","css","memo"],"sources":["../src/client/config.ts","../src/client/locales.ts","../src/client/timeline-kinds.ts","../src/client/rail-tracker.ts","../src/client/chat-hook.ts","../src/client/preview.ts","../src/client/hidden-spans.ts","../src/client/TimelineRail.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * Browser-half configuration of the message-timeline plugin. The runner hands\n * the plugin its validated entry config through the apply second parameter;\n * this module supplies the defaults and clamps, so a composition that does not\n * pass config still renders with the documented values and out-of-range input\n * lands inside the legal bounds.\n */\n\n/** Deployment-tunable rail behavior. */\nexport interface TimelineConfig {\n /** Master switch: false hides the toggle and the rail entirely. */\n enabled: boolean\n /** Count steering messages (user text admitted mid-turn) as rows. */\n includeSteering: boolean\n /**\n * Preferred timeline panel width in px (clamped 120–640; long text\n * ellipsizes). The panel's right edge never crosses the message flow: the\n * width is capped by the scrollport's left gutter, so a narrow column\n * shrinks the panel automatically, and a gutter too small for\n * {@link PANEL_WIDTH_MIN} hides the panel entirely.\n */\n panelWidth: number\n /**\n * History pages to prefetch when the rail opens (50 events each); older\n * pages load on demand when the rail or the panel is scrolled to its top.\n */\n initialPages: number\n}\n\n/** Bounds of the deployment-tunable numbers (clamped in resolveConfig). */\nexport const PANEL_WIDTH_MIN = 120\nexport const PANEL_WIDTH_MAX = 640\nconst INITIAL_PAGES_MIN = 1\nconst INITIAL_PAGES_MAX = 20\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\n/**\n * Normalize the entry config into the full {@link TimelineConfig}: every\n * omitted field takes its documented default and every numeric field is\n * clamped into its legal range.\n * @param config - the unvalidated entry config, when the runner passes one.\n * @returns the effective rail behavior.\n */\nexport function resolveConfig(config: Partial<TimelineConfig> | undefined): TimelineConfig {\n return {\n enabled: config?.enabled ?? true,\n includeSteering: config?.includeSteering ?? true,\n panelWidth: clamp(config?.panelWidth ?? 360, PANEL_WIDTH_MIN, PANEL_WIDTH_MAX),\n initialPages: clamp(config?.initialPages ?? 5, INITIAL_PAGES_MIN, INITIAL_PAGES_MAX),\n }\n}\n","/** `message-timeline` namespace dictionaries. */\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'rail.panel': '消息导览',\n 'rail.empty': '暂无用户消息',\n} satisfies Record<string, string>\n\n/** The message-timeline namespace key union. */\nexport type TimelineKey = keyof typeof zh\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The message timeline panel's copy. */\n 'message-timeline': TimelineKey\n }\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en = {\n 'rail.panel': 'Message timeline',\n 'rail.empty': 'No user messages',\n} satisfies Record<TimelineKey, string>\n","/**\n * Timeline node-kind classification. The rail's projection (which store nodes\n * become rows) and its DOM tracking (which flow rows read as the reading\n * position) must agree on the set of \"timeline row\" kinds, and the hidden-\n * span fold must agree on the set of \"span carrier\" kinds. Rather than repeat\n * these inline predicates at every site — where a future kind could be added\n * to one place and silently missed in another — they live here as the single\n * source of truth. Both are pure and read only the `kind` string, so they hold\n * no dependency on the message-tools package and work for any session.\n */\n\n/** A message-tools edited-bubble row (an in-place edit replacement). */\nexport const EDIT_KIND = 'message-tools-edited'\n/** A message-tools restored row (a withdrawn message replayed at the tail). */\nexport const RESTORED_KIND = 'message-tools-restored'\n\n/**\n * Whether a node kind renders as a timeline row. The rail lists user messages\n * (and optionally steering), plus the message-tools edited/restored bubbles\n * that the host `order` does not always surface.\n * @param kind - the node's kind string.\n * @param includeSteering - whether steering rows count as user messages.\n * @returns true when the kind belongs on the timeline.\n */\nexport function isTimelineRowKind(kind: string, includeSteering: boolean): boolean {\n return kind === 'user' || (includeSteering && kind === 'steering')\n || kind === EDIT_KIND || kind === RESTORED_KIND\n}\n\n/**\n * Whether a node kind carries a hidden span (`{ hiddenStartSeq, seq }`) that\n * the rail must fold to drop the covered originals. The withdrawal divider and\n * the edited bubble both declare the span they shadow.\n * @param kind - the node's kind string.\n * @returns true when the kind declares a hidden span.\n */\nexport function isHiddenSpanCarrierKind(kind: string): boolean {\n return kind === 'message-tools-withdrawn' || kind === EDIT_KIND\n}\n","/**\n * Rail DOM tracker, apply world. The floating timeline panel needs the\n * official chat scrollport's live geometry and the currently visible\n * user-message row, so\n * this module owns the only DOM the plugin touches — read-only probes plus\n * the scroll write the jump performs — and publishes the result through the\n * reserved hooks compartment (components never see the DOM or the sources).\n *\n * The probed attributes ([data-conversation-scroll], [data-chat-anchor-key],\n * [data-chat-flow-kind]) are official render output, not a declared API: when\n * they change, the tracker degrades — a missing scrollport hides the rail\n * (one console.warn), missing rows only clear the active marker and make\n * jumps no-ops. Nothing throws and no official code is modified.\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'\nimport type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'\n// Type-only: pulls the ctx.sessions service merge.\nimport type {} from '@deepseek-ai/dsh-api-session-controller/client'\nimport type { TimelineRailState } from './slots.ts'\nimport { isTimelineRowKind } from './timeline-kinds.ts'\n\n/** Horizontal inset of the rail from the scrollport's left edge (px). */\nconst RAIL_LEFT_INSET = 6\n/** Vertical padding above and below the rail inside the scrollport (px). */\nconst RAIL_VERTICAL_PADDING = 8\n/** Keep the target row this far below the scrollport top after a jump (px). */\nconst JUMP_OFFSET = 16\n\n/** Idle state published before any session binds or while none is current. */\nconst IDLE: TimelineRailState = {\n sessionId: undefined, ready: false, left: 0, top: 0, height: 0, scrollportWidth: 0, flowLeft: null,\n activeKey: null, chatView: false,\n}\n\n/**\n * Measure the panel's viewport box from one scrollport: its rect inset by the\n * panel padding, minus the chat input card at the bottom and the conversation\n * tab strip at the top. The tabs render just above the scrollport, but\n * centering reads against the whole window, so the strip height leaves the\n * box either way — otherwise the list sits visibly high.\n * The bottom ends at the `[data-composer-card]` top — the chat box — NOT the\n * whole `[data-composer-seat]` top: dock cards (goal/todo/queue) sit above\n * the input inside the seat, and counting them would push the timeline up off\n * the conversation. Falls back to the seat top, then the column bottom, when\n * the markers are absent.\n * The scrollport's own width rides along for the width-cap fallback when the\n * message-flow probe is unanswered.\n * @param scrollport - the official conversation scrollport element.\n * @returns the panel box plus the scrollport width, or null while the\n * scrollport has no laid-out size.\n */\nexport function measureGeometry(scrollport: HTMLElement): { left: number; top: number; height: number; width: number } | null {\n const rect = scrollport.getBoundingClientRect()\n if (rect.width === 0 && rect.height === 0) return null\n let topInset = RAIL_VERTICAL_PADDING\n for (const tabs of scrollport.ownerDocument.querySelectorAll<HTMLElement>('[role=\"tablist\"]')) {\n const tabsRect = tabs.getBoundingClientRect()\n // Only the strip adjacent to the scrollport's top edge counts as chrome.\n if (tabsRect.height > 0 && Math.abs(tabsRect.bottom - rect.top) <= 80) {\n topInset += tabsRect.height\n break\n }\n }\n const card = scrollport.querySelector<HTMLElement>('[data-composer-card]')\n const seat = scrollport.querySelector<HTMLElement>('[data-composer-seat]')\n const bottom = card !== null\n ? card.getBoundingClientRect().top - RAIL_VERTICAL_PADDING\n : seat !== null\n ? seat.getBoundingClientRect().top - RAIL_VERTICAL_PADDING\n : rect.bottom - RAIL_VERTICAL_PADDING\n return {\n left: rect.left + RAIL_LEFT_INSET,\n top: rect.top + topInset,\n height: Math.max(0, bottom - rect.top - topInset),\n width: rect.width,\n }\n}\n\n/**\n * The viewport x of the message flow's left edge: the left of the first\n * laid-out `[data-chat-flow-kind]` row, which sits flush inside the official\n * centered content column (max 748px, `margin: 0 auto`). Laid-out flow rows\n * share that edge, so the first meaningful one suffices. The panel's right\n * edge stays left of it — the panel may only occupy the scrollport's left\n * gutter.\n *\n * Rows that are not laid out in the flow are skipped: a message-tools edit\n * leaves the withdrawn originals in the DOM (hidden, zero-size, or off the\n * column at x=0), and probing their left edge yields 0 — which would make the\n * width gate compute a negative left gutter and hide the entire rail. Any\n * real flow row sits inside the conversation column at a positive x.\n * @param scrollport - the official conversation scrollport element.\n * @returns the flow's left edge, or null while no laid-out flow row is rendered.\n */\nexport function flowLeftX(scrollport: HTMLElement): number | null {\n for (const row of scrollport.querySelectorAll<HTMLElement>('[data-chat-flow-kind]')) {\n const rect = row.getBoundingClientRect()\n if (rect.width === 0 && rect.height === 0) continue\n if (rect.left <= 0) continue\n return rect.left\n }\n return null\n}\n\n/**\n * Resolve the key of the user-message row the reading position belongs to:\n * the first matching row whose bottom is still inside the viewport, or —\n * while the reader sits inside a long assistant answer with no user row\n * visible — the nearest user row above the viewport, so the lit tick stays\n * anchored to the question being answered instead of jumping to the\n * session's latest message.\n * @param scrollport - the official conversation scrollport element.\n * @param includeSteering - whether steering rows count as user messages.\n * @returns the row's anchor key, or null when no user row is rendered.\n */\nexport function activeRowKey(scrollport: HTMLElement, includeSteering: boolean): string | null {\n const viewTop = scrollport.getBoundingClientRect().top\n let lastAbove: string | null = null\n for (const row of scrollport.querySelectorAll<HTMLElement>('[data-chat-flow-kind]')) {\n const kind = row.dataset.chatFlowKind\n if (kind === undefined || !isTimelineRowKind(kind, includeSteering)) continue\n if (row.getBoundingClientRect().bottom <= viewTop) {\n lastAbove = row.dataset.chatAnchorKey ?? lastAbove\n continue\n }\n return row.dataset.chatAnchorKey ?? null\n }\n return lastAbove\n}\n\n/**\n * Scroll one user-message row to the top of the transcript scrollport.\n * @param scrollport - the official conversation scrollport element.\n * @param key - the target node's anchor key.\n * @returns whether the row was found and scrolled.\n */\nexport function jumpRow(scrollport: HTMLElement, key: string): boolean {\n const row = scrollport.querySelector<HTMLElement>(`[data-chat-anchor-key=${JSON.stringify(key)}]`)\n if (row === null) return false\n const target = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top\n + scrollport.scrollTop - JUMP_OFFSET\n scrollport.scrollTop = Math.max(0, target)\n return true\n}\n\n/** The tracker's outward face: the observable state, the jump verb, and the disposer. */\nexport interface RailTracker {\n /** Live rail geometry and active marker for the current session. */\n readonly state: HostObservable<TimelineRailState>\n /** Scroll the transcript to the message addressed by `key` (no-op while unbound). */\n jumpTo(key: string): void\n /** Unbind every listener and observer. */\n dispose(): void\n}\n\n/**\n * Install the rail tracker: bind the current session's scrollport, publish\n * geometry/active updates on scroll and resize, and answer jump requests.\n * Follows the current session through the sessions list and provide channels\n * (the interface renders one conversation at a time, so one tracker suffices).\n * @param ctx - client root context (sessions service).\n * @param includeSteering - whether steering rows count as user dots.\n * @returns the tracker face.\n */\nexport function installRailTracker(ctx: Context, includeSteering: boolean): RailTracker {\n const listeners = new Set<() => void>()\n let state: TimelineRailState = IDLE\n let activeSession: SessionId | undefined\n let scrollport: HTMLElement | null = null\n let resizeObserver: ResizeObserver | undefined\n let rafPending = false\n let warned = false\n // Bind-generation token: every teardown (a rebind or a dispose) bumps it, so\n // a still-pending bind frame that closed over an older token becomes stale\n // and bails. Without this, a bind scheduled before a dispose/rebind would\n // run afterward — binding the global scrollport under a stale session or,\n // worse, re-adding listeners and a ResizeObserver to a disposed tracker.\n let bindToken = 0\n\n const nextFrame = typeof requestAnimationFrame === 'function'\n ? requestAnimationFrame\n : (callback: () => void): void => { setTimeout(callback, 16) }\n\n const same = (left: TimelineRailState, right: TimelineRailState): boolean =>\n left.sessionId === right.sessionId && left.ready === right.ready\n && left.left === right.left && left.top === right.top\n && left.height === right.height && left.scrollportWidth === right.scrollportWidth\n && left.flowLeft === right.flowLeft\n && left.activeKey === right.activeKey\n && left.chatView === right.chatView\n\n const publish = (next: TimelineRailState): void => {\n if (same(state, next)) return\n state = next\n for (const fn of [...listeners]) fn()\n }\n\n const attach = (el: HTMLElement): void => {\n if (scrollport !== null) scrollport.removeEventListener('scroll', onScroll)\n resizeObserver?.disconnect()\n scrollport = el\n scrollport.addEventListener('scroll', onScroll, { passive: true })\n if (typeof ResizeObserver === 'function') {\n resizeObserver = new ResizeObserver(scheduleUpdate)\n resizeObserver.observe(el)\n const composer = el.querySelector<HTMLElement>('[data-composer-seat]')\n if (composer !== null) resizeObserver.observe(composer)\n }\n }\n\n const update = (): void => {\n // Self-heal the scrollport binding: while unbound (or bound to a node the\n // host remounted away — the conversation view re-renders under a stable\n // session), re-probe the DOM. On rc hosts currentProvideInfo announced\n // the remount; on 0.1.2 the feed is gone, so the observer/resize/scroll\n // cadence that already lands here owns re-resolution on both lines.\n let el = scrollport\n if (el === null || !el.isConnected) {\n if (activeSession === undefined) return\n const found = document.querySelector<HTMLElement>('[data-conversation-scroll]')\n if (found === null) return\n attach(found)\n el = found\n }\n const geometry = measureGeometry(el)\n if (geometry === null) return\n publish({\n sessionId: activeSession,\n ready: true,\n left: geometry.left,\n top: geometry.top,\n height: geometry.height,\n scrollportWidth: geometry.width,\n flowLeft: flowLeftX(el),\n activeKey: activeRowKey(el, includeSteering),\n chatView: el.querySelector('[data-chat-flow]') !== null,\n })\n }\n\n const scheduleUpdate = (): void => {\n if (rafPending) return\n rafPending = true\n nextFrame(() => {\n rafPending = false\n update()\n })\n }\n\n const onScroll = (): void => { scheduleUpdate() }\n\n const teardownBind = (): void => {\n bindToken++\n if (scrollport !== null) scrollport.removeEventListener('scroll', onScroll)\n scrollport = null\n resizeObserver?.disconnect()\n resizeObserver = undefined\n rafPending = false\n }\n\n const bind = (sessionId: SessionId | undefined): void => {\n if (sessionId === activeSession && scrollport !== null) return\n teardownBind()\n activeSession = sessionId\n if (sessionId === undefined) {\n publish(IDLE)\n return\n }\n const token = bindToken\n nextFrame(() => {\n // This frame was scheduled by an earlier bind whose generation has since\n // been superseded by a rebind or a dispose: ignore it entirely rather\n // than binding the scrollport under a stale session or binding into a\n // disposed tracker.\n if (token !== bindToken) return\n const found = document.querySelector<HTMLElement>('[data-conversation-scroll]')\n if (found === null) {\n if (!warned) {\n warned = true\n console.warn('message-timeline: [data-conversation-scroll] not found; the rail stays hidden')\n }\n publish(IDLE)\n return\n }\n attach(found)\n update()\n })\n }\n\n const bindCurrent = (): void => {\n bind(ctx.sessions.list.getSnapshot().current)\n }\n const stopList = ctx.sessions.list.subscribe(bindCurrent)\n // Host 0.1.2-alpha.1 removed ISessions.currentProvideInfo (commit\n // be531688f3 — the provide-channel projection went with the runtime\n // package). rc hosts still publish it, and it is the precise remount\n // signal there, so subscribe when the feed exists and skip it when the\n // service predates/omits it — update() below re-resolves a missing or\n // detached scrollport from the MutationObserver/resize cadence, which\n // covers the remount signal on both lines.\n const provideFeed = (ctx.sessions as { currentProvideInfo?: HostObservable<unknown> }).currentProvideInfo\n const stopProvide = provideFeed?.subscribe(bindCurrent)\n bindCurrent()\n\n // Layout fallbacks beyond the scrollport's own ResizeObserver: a window\n // resize re-measures, and a body MutationObserver catches panel folds and\n // other layout changes that resize the scrollport without a window event.\n // Both go through the rAF-throttled scheduleUpdate, and publish() skips\n // identical geometry, so the cost stays one measurement per changed frame.\n const onWindowResize = (): void => { scheduleUpdate() }\n if (typeof window !== 'undefined') window.addEventListener('resize', onWindowResize)\n let mutationObserver: MutationObserver | undefined\n if (typeof MutationObserver === 'function' && typeof document !== 'undefined') {\n mutationObserver = new MutationObserver(scheduleUpdate)\n mutationObserver.observe(document.body, { childList: true, subtree: true })\n }\n\n return {\n state: {\n getSnapshot: () => state,\n subscribe: (fn) => {\n listeners.add(fn)\n return () => { listeners.delete(fn) }\n },\n },\n jumpTo: (key) => {\n if (scrollport !== null) jumpRow(scrollport, key)\n },\n dispose: () => {\n stopList()\n stopProvide?.()\n if (typeof window !== 'undefined') window.removeEventListener('resize', onWindowResize)\n mutationObserver?.disconnect()\n teardownBind()\n },\n }\n}\n","/**\n * Chat snapshot hook seat. Host 0.1.2 split the chat snapshot out of the\n * session snapshot into the `useChat` session standard prop (ui-chat); its\n * snapshot carries the fields the panel reads (`order`, `nodes`) at the top\n * level. The rc-line `useSession(...).chat` seat went away with the baseline\n * flip, so there is no second arm: the helper reads `useChat` and degrades to\n * an empty slice (panel hidden, never a throw) when the prop is absent.\n */\nimport type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client'\nimport type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'\n\n/**\n * The chat slice the panel's selectors read — the narrow face of ui-chat's\n * ChatSnapshot the rail depends on (a structural subset, so the official\n * `UseChat` hook is directly assignable).\n */\nexport interface ChatSlice {\n /** Visible chat node keys in render order. */\n readonly order: readonly string[]\n /** Live per-key node store. */\n readonly nodes: {\n get(key: string): ChatConversationViewNode | undefined\n values(): readonly ChatConversationViewNode[]\n }\n}\n\n/** Selector hook over the chat slice. */\nexport type ChatSliceHook = SnapshotSelectorHook<ChatSlice>\n\n/** Empty slice behind the absent-prop degrade: no rows, so the panel hides. */\nconst EMPTY_SLICE: ChatSlice = {\n order: [],\n nodes: { get: () => undefined, values: () => [] },\n}\n\n/** Constant hook answering every selector from the empty slice. */\nconst EMPTY_HOOK: ChatSliceHook = <S,>(selector: (chat: ChatSlice) => S): S => selector(EMPTY_SLICE)\n\n/**\n * Pick the live chat hook for this entry.\n * @param props - the entry's composed props (useChat present on 0.1.2 hosts).\n * @returns the `useChat` prop, or the empty-slice hook when the seat is absent.\n */\nexport function chatHookOf(props: { useChat?: SnapshotSelectorHook<ChatSlice> }): ChatSliceHook {\n return props.useChat ?? EMPTY_HOOK\n}\n","/**\n * Pure text extraction for the timeline panel rows: user message content is a\n * block list (text, image, …); the preview concatenates the text blocks so\n * long messages ellipsize through CSS instead of dropping their first lines.\n */\nimport type { ContentBlock } from '@deepseek-ai/dsh-llm'\n\n/**\n * Join the text blocks of one message into a single preview string.\n * @param content - the message's content block list.\n * @returns the concatenated text, or null when the message carries none.\n */\nexport function previewText(content: readonly ContentBlock[]): string | null {\n const parts: string[] = []\n for (const block of content) {\n if (block.type === 'text' && block.text !== '') parts.push(block.text)\n }\n return parts.length === 0 ? null : parts.join('\\n')\n}\n","/**\n * Hidden-span folding for the rail. A message-tools withdraw (or an in-place\n * edit) shadows a span of the transcript but leaves the withdrawn original\n * rows in the node store — and, because the projection offers no suppression\n * seam, a withdrawn user message stays visible in the host `order` while the\n * sibling DOM hider hides its row by CSS. A rail row for such a node is a dead\n * row: the transcript row it would target is `display:none`, so a click cannot\n * scroll to it.\n *\n * The span a withdraw or edit covers is carried right on the store by the\n * sibling `message-tools-withdrawn` / `message-tools-edited` node as\n * `{ hiddenStartSeq, seq }` (its `anchorSeq` is `seq`, the exclusive end, so\n * the divider/edited bubble itself is never inside its own span). Folding\n * those spans — exactly the pair list message-tools' `foldHiddenRanges`\n * builds — lets the rail skip any row whose `anchorSeq` falls inside one.\n *\n * This module reads only generic store node data (no import from message-tools\n * and no dependence on its type map). An ordinary session carries no such\n * node, so the fold is empty and every order row still renders: the fix never\n * alters the baseline path.\n */\nimport type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client'\nimport { isHiddenSpanCarrierKind } from './timeline-kinds.ts'\n\n/** Span data carried by a `message-tools-withdrawn` / `message-tools-edited` node. */\ninterface SpanCarrier {\n readonly hiddenStartSeq?: number\n readonly seq?: number\n}\n\n/**\n * Fold the hidden seq spans out of the store into the flat\n * `[start, endExclusive, ...]` pair list ordered by start — the same shape\n * message-tools' renderers and DOM hider consume (`{ hiddenStartSeq, seq }`,\n * inclusive start, exclusive end). A malformed carrier (missing or inverted\n * bounds) is skipped rather than poisoning the fold.\n * @param nodes - every materialized chat node (any order).\n * @returns the flattened hidden spans; empty when no withdraw/edit landed.\n */\nexport function foldHiddenSpans(nodes: readonly ChatConversationViewNode[]): number[] {\n const pairs: Array<[number, number]> = []\n for (const node of nodes) {\n if (!isHiddenSpanCarrierKind(node.kind)) continue\n const data = node.data as SpanCarrier\n if (data.hiddenStartSeq === undefined || data.seq === undefined) continue\n if (data.hiddenStartSeq >= data.seq) continue\n pairs.push([data.hiddenStartSeq, data.seq])\n }\n pairs.sort((left, right) => left[0] - right[0])\n return pairs.flat()\n}\n\n/**\n * Whether a node seq falls inside one of the folded hidden spans.\n * @param spans - the flattened spans from {@link foldHiddenSpans}.\n * @param seq - the node seq to test.\n * @returns true when the seq is hidden (a withdrawn original).\n */\nexport function isSeqHidden(spans: readonly number[], seq: number): boolean {\n for (let index = 0; index + 1 < spans.length; index += 2) {\n const start = spans[index]\n const end = spans[index + 1]\n if (start !== undefined && end !== undefined && seq >= start && seq < end) return true\n }\n return false\n}\n","/**\n * Message timeline panel, browser half. One entry in the official\n * conversation.session.header.utilities seat anchors the plugin into the\n * session scope and renders, through a body portal, the flat floating\n * timeline over the left edge of the chat scrollport: one row per loaded\n * user message — a tick plus an ellipsized one-line preview, no frame and no\n * visible scrollbar. At rest only the dimmed ticks show, the reading\n * position's tick in blue (the latest message until the tracker answers);\n * hovering or keyboard-focusing the panel reveals the row texts with the\n * blue row on top, and clicking a row jumps the transcript to that message.\n * The panel is always on while the chat view shows; the `enabled` config is\n * the off switch.\n */\nimport { memo, useEffect, useMemo, useRef, useState, type UIEvent } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { ContentBlock } from '@deepseek-ai/dsh-llm'\nimport type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client'\nimport { PANEL_WIDTH_MIN } from './config.ts'\nimport { chatHookOf } from './chat-hook.ts'\nimport type { TimelineItem, TimelineRailProps } from './slots.ts'\nimport { previewText } from './preview.ts'\nimport { foldHiddenSpans, isSeqHidden } from './hidden-spans.ts'\nimport { isTimelineRowKind, EDIT_KIND, RESTORED_KIND } from './timeline-kinds.ts'\nimport css from './TimelineRail.module.css'\n\n/** Extract the preview blocks of one user/steering node (kind-checked by the caller). */\nfunction nodeContent(node: { data: unknown }): readonly ContentBlock[] {\n return (node.data as { content?: readonly ContentBlock[] }).content ?? []\n}\n\n/**\n * Horizontal panel padding, matching `.panel`'s `padding: 4px 8px`: the text\n * band sits this far inside the box, so the width budget must give it back\n * for {@link PANEL_GAP} to be the visible gap to the message flow.\n */\nconst PANEL_PADDING_X = 8\n/** Visible breathing gap between the timeline text and the message flow (px). */\nconst PANEL_GAP = 16\n/** Degraded width cap (fraction of the scrollport) while the flow probe is unanswered. */\nconst DEGRADED_WIDTH_RATIO = 0.4\n\n/**\n * The header-utilities entry: the portal timeline panel.\n * @param props - composed props (see {@link TimelineRailProps}).\n * @returns nothing visible in the seat; the floating panel while the chat view shows.\n */\nexport function TimelineRail({\n sessionId,\n includeSteering, panelWidth, initialPages,\n loadOlder, jumpTo, useRail, t,\n ...standard\n}: TimelineRailProps) {\n const rail = useRail(s => s)\n // Chat data lives in the `useChat` standard prop on 0.1.2 (the rc-line\n // session-snapshot seat is gone); the helper degrades to an empty slice\n // when the seat is absent.\n const useChatSlice = chatHookOf(standard)\n const useSession = standard.useSession\n const order = useChatSlice(c => c.order)\n const nodes = useChatSlice(c => c.nodes)\n const hasMore = useSession(s => s.hasMore)\n const loadingOlder = useSession(s => s.loadingOlder)\n\n const items = useMemo<TimelineItem[]>(() => {\n // The node store is an in-memory snapshot; take it once so the span fold\n // and the message-tools append read the same snapshot.\n let store: readonly ChatConversationViewNode[] = []\n let spans: number[] = []\n try {\n store = nodes.values()\n spans = foldHiddenSpans(store)\n } catch {\n // Degrade, don't explode: a failed store read leaves no spans and no\n // appended bubbles, so the baseline order rows still render (a withdraw\n // span reappears, and bubbles re-appear, after the store recovers).\n }\n // A message-tools withdraw (or an in-place edit) leaves the withdrawn\n // originals in the store with a sibling `message-tools-withdrawn` /\n // `message-tools-edited` node carrying the span they cover as\n // `{ hiddenStartSeq, seq }`. Those originals can still surface in the\n // host's `order`, but the DOM hider hides their transcript rows — a rail\n // row for one is a dead row (clicking cannot scroll to it). Drop any such\n // row whose anchorSeq sits inside a span, from either loop below. An\n // ordinary session carries no span node, so the fold is empty and every\n // order row still renders: this filter never alters the baseline.\n const hidden = (node: { anchorSeq: number }): boolean => isSeqHidden(spans, node.anchorSeq)\n const result: TimelineItem[] = []\n const seen = new Set<string>()\n for (const key of order) {\n const node = nodes.get(key)\n if (node === undefined) continue\n if (!isTimelineRowKind(node.kind, includeSteering)) continue\n if (hidden(node)) continue\n result.push({ key, node })\n seen.add(key)\n }\n // After a message-tools in-place edit the replacement materializes as a\n // `message-tools-edited` bubble (a restore as `message-tools-restored`)\n // that the host's visible `order` does not always surface, even though it\n // renders in the flow — an edit of the first message would drain the rail\n // to zero rows. Append any such visible bubble the order omitted, provided\n // it is not itself a withdrawn original (inside a span). Only these two\n // bubble kinds are appended — a plain user/steering node not in `order`\n // must never be resurrected as an orphan row. Normal sessions carry no\n // such nodes, so this never alters the baseline.\n for (const node of store) {\n if (node.kind !== EDIT_KIND && node.kind !== RESTORED_KIND) continue\n if (node.visibility === 'hidden') continue\n if (seen.has(node.key)) continue\n if (hidden(node)) continue\n result.push({ key: node.key, node })\n seen.add(node.key)\n }\n // The order loop is seq-sorted, but the appended bubbles walked the store\n // (whose iteration order is not seq-ordered), so a bubble for an OLDER\n // message would otherwise land at the very end, past the newest row.\n // Sort the combined rows by anchorSeq so every row — original or appended\n // — sits at its true transcript position. JS sort is stable, and an\n // ordinary session has no appended bubbles, so this never reorders the\n // baseline (which is already seq-sorted). Dropped rows are gone before the\n // sort, so a withdrawn original never resurfaces.\n result.sort((left, right) => left.node.anchorSeq - right.node.anchorSeq)\n return result\n }, [order, nodes, includeSteering])\n\n // The lit row: the hover/arrow preselection while it moves, otherwise the\n // live reading position the tracker publishes, defaulting to the latest\n // message before the tracker answers. ArrowUp/Down or hovering moves a\n // bold preselection; Enter or a click confirms it and jumps.\n const [focusKey, setFocusKey] = useState<string | null>(null)\n const current = focusKey ?? rail.activeKey ?? items.at(-1)?.key ?? null\n const panelRef = useRef<HTMLDivElement | null>(null)\n\n // The panel is a chat-view affordance: hide it while the session shows\n // another tab (trajectory etc.), detected through ChatView's data-chat-flow\n // marker. `active` splits from `visible` so history paging can bootstrap: a\n // session whose loaded event window holds no user message yet (a huge\n // assistant turn pushed it past the first page) renders no rows but must\n // still pull pages until one materializes.\n const active = rail.chatView && rail.ready && rail.sessionId === sessionId\n const visible = active && items.length > 0\n\n // The panel must never cover the message flow: its width is the configured\n // preferred width capped by the scrollport's left gutter — the message\n // flow's left edge minus the panel's left edge, less the panel's right\n // padding and the visible breathing gap. A gutter too small for the minimum\n // usable width hides the panel entirely (the timeline is an overlay\n // affordance; squeezed into nothing it only intercepts the transcript).\n // When the flow probe is unanswered (official structure change), the width\n // degrades to a fraction of the scrollport instead — never throws, never\n // covers more than the fallback.\n const gutter = rail.flowLeft === null ? null : rail.flowLeft - rail.left - PANEL_PADDING_X - PANEL_GAP\n const width = gutter === null\n ? Math.min(panelWidth, Math.max(PANEL_WIDTH_MIN, rail.scrollportWidth * DEGRADED_WIDTH_RATIO))\n : Math.min(panelWidth, Math.max(0, gutter))\n const tooNarrow = gutter !== null && width < PANEL_WIDTH_MIN\n\n // Keep the lit row in view: the panel follows the reading position (a new\n // message scrolls its row in), and mouse browsing is never yanked because\n // the hovered row is the current one and always visible under the pointer.\n useEffect(() => {\n panelRef.current?.querySelector<HTMLElement>(`[data-item-key=${JSON.stringify(current)}]`)\n ?.scrollIntoView({ block: 'nearest' })\n }, [current])\n\n // Prefetch history while the panel is on a chat view: keep pulling pages\n // until the first user message materializes (bootstrap), then until\n // initialPages pages arrived, so the list starts near-complete. Older\n // pages load on demand when the panel scrolls to its top.\n const prefetchedPagesRef = useRef(0)\n useEffect(() => {\n if (!active) {\n prefetchedPagesRef.current = 0\n return\n }\n if (!hasMore || loadingOlder) return\n if (items.length > 0 && prefetchedPagesRef.current >= initialPages) return\n prefetchedPagesRef.current += 1\n void loadOlder()\n }, [active, hasMore, loadingOlder, items.length, initialPages, loadOlder])\n\n // Older history loads by scrolling the panel to its top; the official chat\n // view owns the load-older button, so the panel stays chromeless.\n const onPanelScroll = (event: UIEvent<HTMLDivElement>): void => {\n if (!hasMore || loadingOlder) return\n if (event.currentTarget.scrollTop <= 8) void loadOlder()\n }\n\n const confirm = (key: string): void => {\n setFocusKey(null)\n jumpTo(key)\n }\n\n if (!visible || tooNarrow) return null\n return createPortal(\n <div\n ref={panelRef}\n className={css.panel}\n role=\"navigation\"\n aria-label={t('rail.panel')}\n tabIndex={0}\n data-timeline-panel=\"\"\n style={{ left: rail.left, top: rail.top, height: rail.height, width }}\n onScroll={onPanelScroll}\n onKeyDown={(event) => {\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault()\n const anchor = items.findIndex(item => item.key === current)\n const next = event.key === 'ArrowDown'\n ? items[Math.min(anchor + 1, items.length - 1)]\n : items[Math.max(anchor - 1, 0)]\n /* v8 ignore next -- a non-empty item list keeps the clamped index inside bounds */\n if (next !== undefined) setFocusKey(next.key)\n } else if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n /* v8 ignore next -- the panel only renders with at least one item, so a current key always exists */\n if (current !== null) confirm(current)\n }\n }}\n onBlur={() => { setFocusKey(null) }}\n >\n {items.map((item) => {\n const isCurrent = item.key === current\n const isFocused = item.key === focusKey\n const className = isFocused\n ? `${css.item} ${css.itemFocused}`\n : isCurrent ? `${css.item} ${css.itemCurrent}` : css.item\n return (\n <button\n key={item.key}\n type=\"button\"\n data-item-key={item.key}\n aria-current={isCurrent || undefined}\n className={className}\n onClick={() => { confirm(item.key) }}\n onMouseEnter={() => { setFocusKey(item.key) }}\n onMouseLeave={() => { setFocusKey(key => key === item.key ? null : key) }}\n >\n <span className={isCurrent ? `${css.tick} ${css.tickCurrent}` : css.tick} aria-hidden=\"true\" />\n <span className={css.itemText}>{previewText(nodeContent(item.node)) ?? t('rail.empty')}</span>\n </button>\n )\n })}\n </div>,\n document.body,\n )\n}\n\n/** Memoized export for the slot machinery (stable component identity). */\nexport const TimelineRailEntry = memo(TimelineRail)\n","/**\n * Message timeline plugin, browser half. Registers one entry into the\n * official `conversation.session.header.utilities` seat (a right-aligned\n * optional-utility slot) that anchors the plugin into the session scope; the\n * component itself renders only through a body portal: the flat floating\n * timeline panel over the chat scrollport's left edge — one row per loaded\n * user message, no frame, no visible scrollbar. The\n * panel reads the session chat snapshot through the framework `useSession`\n * hook, jumps through the official row anchor attributes, and degrades to a\n * hidden panel when those attributes change — no official code is modified.\n * @module @khorsheed/dsh-message-timeline/client\n */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only: pulls the ctx.locale service merge.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls ui-conversation's SlotMap merge (the header utilities seat)\n// and the conversation service merge.\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n// Type-only: pulls the ctx.slots service merge.\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\n// Type-only: pulls the ctx.sessions service merge.\nimport type {} from '@deepseek-ai/dsh-api-session-controller/client'\n// Type-only: pulls the SessionStandardProps merge (useSession / sessionId).\nimport type {} from '@deepseek-ai/dsh-client-ui-session/client'\n// Type-only: pulls the useChat SessionStandardProps merge.\nimport type {} from '@deepseek-ai/dsh-client-ui-chat/client'\nimport { resolveConfig, type TimelineConfig } from './config.ts'\nimport { en, zh } from './locales.ts'\nimport { installRailTracker } from './rail-tracker.ts'\nimport type { TimelineRailInjected } from './slots.ts'\nimport { TimelineRailEntry } from './TimelineRail.tsx'\n\nexport type { TimelineConfig } from './config.ts'\nexport { resolveConfig } from './config.ts'\nexport type { TimelineKey } from './locales.ts'\nexport { previewText } from './preview.ts'\nexport type { TimelineItem, TimelineRailInjected, TimelineRailProps, TimelineRailState } from './slots.ts'\n\n/** Dictionary namespace owned by this plugin. */\nconst NS = 'message-timeline'\n\n/** Required services: the slot ledger, the session store, and the copy. */\nexport const inject = ['slots', 'sessions', 'locale']\n\n/**\n * Client plugin body: register the header-utilities entry and install the DOM\n * tracker that publishes rail geometry and answers jumps.\n * @param ctx - client root context.\n * @param config - entry config; defaults apply when the runner passes none.\n */\nexport function apply(ctx: Context, config?: Partial<TimelineConfig>): void {\n const options = resolveConfig(config)\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'message-timeline: dictionaries')\n if (!options.enabled) return\n\n const tracker = installRailTracker(ctx, options.includeSteering)\n ctx.effect(() => () => { tracker.dispose() }, 'message-timeline: rail tracker')\n\n // The slot is declared by ui-conversation, whose apply order relative to\n // this plugin is unconstrained: register through slots.inject so the entry\n // waits for the declaration instead of crashing the loader at boot.\n ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({\n name: 'conversation.session.header.utilities',\n id: 'message-timeline',\n order: 100,\n locale: NS,\n inject: (sessionId): TimelineRailInjected => {\n const actx = ctx.sessions.scope(sessionId)\n if (actx === undefined) throw new Error('message-timeline: session resolved no scope')\n const conversation = actx.get('conversation')\n if (conversation === undefined) throw new Error('message-timeline: conversation service unavailable')\n return {\n includeSteering: options.includeSteering,\n panelWidth: options.panelWidth,\n initialPages: options.initialPages,\n loadOlder: () => conversation.loadOlder(),\n jumpTo: (key) => { tracker.jumpTo(key) },\n hooks: { rail: tracker.state },\n }\n },\n }, TimelineRailEntry))\n}\n"],"mappings":"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"}