@dxos/react-ui-feed 0.14.0 → 0.16.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.
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-src.mjs","names":[],"sources":["../../src/hooks/useDecorations.tsx","../../src/hooks/useFeedNavigation.ts","../../src/hooks/useItemSelection.tsx","../../src/components/Block/selection-group.ts","../../src/components/Block/highlight.ts","../../src/components/Block/widget-state.tsx","../../src/components/Block/HtmlBlock.tsx","../../src/components/Block/diff-range.ts","../../src/components/Block/extensions.ts","../../src/components/Block/MarkdownBlock.tsx","../../src/model/feed-model.ts","../../src/model/feed.ts","../../src/model/useListModel.ts","../../src/components/MessageList/position-log.ts","../../src/components/MessageList/MessageList.tsx","../../src/components/Minimap/Minimap.tsx","../../src/components/Outline/Outline.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { type PropsWithChildren, createContext, useContext, useMemo } from 'react';\n\n/**\n * A range of one item's rendered text that some producer wants painted (SPEC: Decoration).\n *\n * Search is ONE producer — mentions, lint results and diff spans are others — which is why this is\n * not called a hit: the item painting the range does not care where it came from, and the list does\n * not know it exists at all.\n */\nexport type Decoration = {\n /** Item (message) id. */\n id: string;\n range: { offset: number; length: number };\n /** Producer tag: 'search' | 'mention' | ... */\n kind: string;\n};\n\nconst DecorationsContext = createContext<ReadonlyMap<string, readonly Decoration[]>>(new Map());\n\nexport type DecorationsProviderProps = PropsWithChildren<{\n decorations?: readonly Decoration[];\n}>;\n\n/**\n * THE PATTERN for cross-cutting per-item data (SPEC §Aspects), not a feed feature: a provider at\n * the host carries what producers made, and an item asks for its own by id. The list neither knows\n * nor routes — `hits` left its API entirely. Grouped once per change rather than filtered per item,\n * so a long feed stays O(decorations).\n */\nexport const DecorationsProvider = ({ decorations, children }: DecorationsProviderProps) => {\n const byId = useMemo(() => {\n const map = new Map<string, Decoration[]>();\n for (const decoration of decorations ?? []) {\n const list = map.get(decoration.id) ?? [];\n list.push(decoration);\n map.set(decoration.id, list);\n }\n\n return map;\n }, [decorations]);\n\n return <DecorationsContext.Provider value={byId}>{children}</DecorationsContext.Provider>;\n};\n\nconst NONE: readonly Decoration[] = [];\n\n/** An item's own decorations. Items work without a provider — they are simply undecorated. */\nexport const useDecorations = (id: string): readonly Decoration[] => {\n return useContext(DecorationsContext).get(id) ?? NONE;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type RefObject, useCallback, useMemo, useRef } from 'react';\n\nimport { type WindowController } from '@dxos/react-ui-virtual';\n\nimport { type Stop } from '../model/index.ts';\n\n/** How long a commanded destination outranks the scroll offset as \"where the reader is\". */\nconst PENDING_WINDOW = 1_500;\n\nexport type UseFeedNavigationOptions = {\n controller: RefObject<WindowController | null>;\n /** The stops policy's current answer; asked per press, so a policy change needs no rewiring. */\n stops: () => readonly Stop[];\n /** The row containing the scroll offset — the cursor, derived and never set beside the scroll. */\n current: () => number;\n count: () => number;\n /**\n * Told before every jump, with the target index — the follow's `onNavigate` goes here. Without\n * it a navigation races the follow's correction effect, which runs before the jump's scroll\n * event can withdraw the intent and snaps the reader straight back to the tail they just left.\n * The follow may move the view only while content is arriving AND the reader has pinned\n * themselves to the bottom; navigating into the feed un-pins by definition.\n */\n onNavigate?: (index: number) => void;\n};\n\nexport type FeedNavigation = {\n /** Move by `delta` stops. Every press travels: see below. */\n step: (delta: number) => void;\n /** Jump to a stop (or a raw index), aligning the last row to the end where that is what it means. */\n jumpTo: (index: number, behavior?: ScrollBehavior) => void;\n first: () => void;\n last: () => void;\n};\n\n/**\n * The one navigation seam (SPEC: useFeedNavigation): toolbar buttons, arrow keys, the outline rail\n * and the minimap all call this, so they cannot disagree about what a step is.\n *\n * Stepping is from the row containing the scroll offset. The stop above it begins strictly higher\n * and the stop below strictly lower, so every press moves the viewport — an index compared against\n * a position cannot promise that while positions are still being measured. Steps glide (corrections\n * move the window, not the scroll, so nothing cancels the animation); far jumps are instant, since\n * a glide across unmounted rows is a journey over a blank screen.\n *\n * Distinct from react-ui-list's `useListNavigation`, which is focus traversal over fully-mounted\n * rows — a different problem in the same naming family.\n */\nexport const useFeedNavigation = ({\n controller,\n stops,\n current,\n count,\n onNavigate,\n}: UseFeedNavigationOptions): FeedNavigation => {\n // The last commanded destination, while its travel may still be in flight. A step during a glide\n // must chain from where the reader is *going*, not from wherever the scroll happens to be this\n // frame — deriving from the offset mid-travel re-finds the same stop, and rapid presses swallow\n // each other (seen live: the toolbar's up button \"not reliably\" stepping). Expires rather than\n // waiting on arrival, because a scroll the reader takes over is theirs again.\n const pending = useRef<{ index: number; at: number } | undefined>(undefined);\n\n // Hosts hand the accessors as inline closures, so their identity churns per render; read through\n // a ref so the seam itself is stable — a controller derived from it and stored in a host's state\n // otherwise republishes every render, which is a setState-in-effect loop.\n const optionsRef = useRef({ stops, current, count, onNavigate });\n optionsRef.current = { stops, current, count, onNavigate };\n\n const jumpTo = useCallback(\n (index: number, behavior: ScrollBehavior = 'auto') => {\n const { count, onNavigate } = optionsRef.current;\n pending.current = { index, at: performance.now() };\n onNavigate?.(index);\n const align = index >= count() - 1 ? 'end' : 'start';\n controller.current?.scrollToIndex(index, align, behavior);\n },\n [controller],\n );\n\n const step = useCallback(\n (delta: number) => {\n const { stops, current } = optionsRef.current;\n const all = stops();\n if (!all.length) {\n return;\n }\n\n const flight = pending.current;\n const at =\n flight && performance.now() - flight.at < PENDING_WINDOW && flight.index !== current()\n ? flight.index\n : current();\n const next =\n delta > 0\n ? (all.find(({ index }) => index > at) ?? all[all.length - 1])\n : ([...all].reverse().find(({ index }) => index < at) ?? all[0]);\n\n jumpTo(next.index, 'smooth');\n },\n [jumpTo],\n );\n\n const first = useCallback(() => jumpTo(0), [jumpTo]);\n\n const last = useCallback(() => {\n const { count } = optionsRef.current;\n jumpTo(count() - 1);\n }, [jumpTo]);\n\n return useMemo(() => ({ step, jumpTo, first, last }), [step, jumpTo, first, last]);\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { type MouseEvent, type PropsWithChildren, createContext, useCallback, useContext } from 'react';\n\n/** What an item can do about its own selection — the item-side face of the host's selection model. */\nexport type ItemSelection = {\n selected: boolean;\n /** Additive when the gesture says so (meta/ctrl); the mode is the provider's business. */\n toggle: (additive?: boolean) => void;\n};\n\nexport type ItemSelectionValue = {\n selectedIds?: ReadonlySet<string>;\n onSelect?: (id: string, additive: boolean) => void;\n};\n\nconst ItemSelectionContext = createContext<ItemSelectionValue>({});\n\nexport type ItemSelectionProviderProps = PropsWithChildren<ItemSelectionValue>;\n\n/**\n * The item-side hook of the decoration pattern applied to selection (SPEC: useItemSelection): an\n * item asks for its own `{ selected, toggle }` by id instead of the list threading `selectedIds`\n * through props. Selection is model-space ids, so virtualization is untouched: an unmounted item is\n * still selected, and mounts knowing it.\n *\n * The provider is deliberately the thinnest thing that works — a set and a callback. Mode policies\n * (single | multi | range) belong to `react-ui-list`'s `useListSelection`, which a host composes\n * *above* this provider; teaching this package about modes would be rebuilding that hook.\n */\nexport const ItemSelectionProvider = ({ selectedIds, onSelect, children }: ItemSelectionProviderProps) => (\n <ItemSelectionContext.Provider value={{ selectedIds, onSelect }}>{children}</ItemSelectionContext.Provider>\n);\n\n/**\n * The whole selection value, for a component that renders many items in one pass.\n *\n * A row loop cannot call `useItemSelection` per row — hooks do not run in loops — so the viewport\n * reads the value once and derives per-row state; item-shaped components use the per-id hook.\n */\nexport const useItemSelectionValue = (): ItemSelectionValue => useContext(ItemSelectionContext);\n\nexport const useItemSelection = (id: string): ItemSelection => {\n const { selectedIds, onSelect } = useContext(ItemSelectionContext);\n const toggle = useCallback((additive = true) => onSelect?.(id, additive), [id, onSelect]);\n return { selected: selectedIds?.has(id) ?? false, toggle };\n};\n\n/** Reads the additive intent off a gesture, so callers do not each invent the modifier rule. */\nexport const isAdditive = (event: MouseEvent | globalThis.MouseEvent): boolean =>\n event.metaKey || event.ctrlKey || event.shiftKey;\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type EditorView } from '@codemirror/view';\nimport { createContext, useContext, useMemo } from 'react';\n\n/**\n * Coordinates selection across the items of one feed.\n *\n * Each item is its own editor with its own selection state, and `drawSelection` paints that state\n * whether or not the view has focus — so without a coordinator every message the reader has ever\n * selected in stays highlighted at once. The feed has one selection, so claiming it in one item\n * collapses it everywhere else.\n */\nexport type SelectionGroup = {\n /** Adds a view to the group; returns the disposer. */\n register: (view: EditorView) => () => void;\n /** Makes `view` the sole owner of the feed's selection. */\n claim: (view: EditorView) => void;\n};\n\n/** Creates a group; one per feed, provided to its items by `MessageList.Root`. */\nexport const createSelectionGroup = (): SelectionGroup => {\n const views = new Set<EditorView>();\n\n return {\n register: (view) => {\n views.add(view);\n return () => views.delete(view);\n },\n\n claim: (view) => {\n for (const other of views) {\n if (other === view || other.state.selection.main.empty) {\n continue;\n }\n // Collapsing re-enters this listener for `other`, but with an empty selection, which claims\n // nothing — so the cascade terminates after one pass.\n other.dispatch({ selection: { anchor: 0 } });\n }\n },\n };\n};\n\nexport const SelectionGroupContext = createContext<SelectionGroup | undefined>(undefined);\n\n/** The enclosing feed's selection group, or a private one when an item is used standalone. */\nexport const useSelectionGroup = (): SelectionGroup => {\n const provided = useContext(SelectionGroupContext);\n const fallback = useMemo(createSelectionGroup, []);\n return provided ?? fallback;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { StateEffect, StateField } from '@codemirror/state';\nimport { Decoration, type DecorationSet, EditorView } from '@codemirror/view';\n\n/** Ranges to highlight, as [from, to) offsets into the item's document. */\nexport type HighlightRange = readonly [number, number];\n\nexport const setHighlights = StateEffect.define<readonly HighlightRange[]>();\n\nconst mark = Decoration.mark({ class: 'dx-feed-hit' });\n\nconst clamp = (offset: number, length: number) => Math.max(0, Math.min(offset, length));\n\n/**\n * Highlights pushed in from the model.\n *\n * Search runs over the feed's projection rather than per-view (most messages are never mounted), so\n * the item receives ranges instead of owning search state.\n */\nexport const highlights = StateField.define<DecorationSet>({\n create: () => Decoration.none,\n update: (value, tr) => {\n for (const effect of tr.effects) {\n if (effect.is(setHighlights)) {\n const length = tr.state.doc.length;\n return Decoration.set(\n effect.value\n // Clamp against the current document at both ends: a hit computed on the model can\n // outrun a view whose streaming tail has not arrived yet, and a decoration outside\n // `[0, length]` throws rather than being ignored.\n .map(([from, to]) => [clamp(from, length), clamp(to, length)] as const)\n .filter(([from, to]) => from < to)\n .map(([from, to]) => mark.range(from, to)),\n true,\n );\n }\n }\n\n return tr.docChanged ? value.map(tr.changes) : value;\n },\n provide: (field) => EditorView.decorations.from(field),\n});\n\nexport const highlightTheme = EditorView.baseTheme({\n '.dx-feed-hit': {\n backgroundColor: 'var(--dx-accentFill)',\n color: 'var(--dx-accentFillText)',\n borderRadius: '2px',\n },\n});\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { type PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from 'react';\n\n/**\n * State a widget keeps that must outlive the row it is drawn in.\n *\n * A virtualized feed destroys a row when it leaves the window, and React state inside a widget dies\n * with it — so a reader who expands a tool panel, scrolls away and comes back finds it shut. In a\n * thread that is one long document the question never arises, because nothing ever unmounts; it is\n * the cost of virtualization, and it has to be paid somewhere above the row.\n *\n * Here, in a map owned by `MessageList.Root`. Not in the message, because whether a panel is open is\n * the reader's business and not part of what was said.\n */\nexport type WidgetStateStore = {\n read: (key: string) => unknown;\n write: (key: string, value: unknown) => void;\n};\n\nconst WidgetStateContext = createContext<WidgetStateStore | undefined>(undefined);\n\n/** Scopes a widget's key to its message, so two messages' widgets cannot collide. */\nconst WidgetScopeContext = createContext<string>('');\n\nexport const WidgetStateProvider = ({ store, children }: PropsWithChildren<{ store: WidgetStateStore }>) => (\n <WidgetStateContext.Provider value={store}>{children}</WidgetStateContext.Provider>\n);\n\nexport const WidgetScopeProvider = ({ scope, children }: PropsWithChildren<{ scope: string }>) => (\n <WidgetScopeContext.Provider value={scope}>{children}</WidgetScopeContext.Provider>\n);\n\n/** Creates the store a feed's widgets share. One per `MessageList.Root`. */\nexport const createWidgetStateStore = (): WidgetStateStore => {\n const values = new Map<string, unknown>();\n return {\n read: (key) => values.get(key),\n write: (key, value) => {\n values.set(key, value);\n },\n };\n};\n\n/**\n * `useState` for a widget, keyed so it survives the row being destroyed and rebuilt.\n *\n * The key must identify the widget within its message — its tag and position in the document, say —\n * and is scoped to the message automatically. Outside a feed this degrades to plain `useState`, so a\n * widget can still be rendered on its own.\n */\nexport const useWidgetState = <T,>(key: string, initial: T): [T, (value: T) => void] => {\n const store = useContext(WidgetStateContext);\n const scope = useContext(WidgetScopeContext);\n const scoped = useMemo(() => `${scope}:${key}`, [scope, key]);\n\n // Seeded from the store on mount, which is the whole point: a remounted widget starts from what\n // the reader last left it at rather than from the default.\n const [value, setValue] = useState<T>(() => {\n const stored = store?.read(scoped);\n return (stored === undefined ? initial : stored) as T;\n });\n\n const set = useCallback(\n (next: T) => {\n store?.write(scoped, next);\n setValue(next);\n },\n [store, scoped],\n );\n\n return [value, set];\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport DOMPurify from 'dompurify';\nimport React, { memo, useMemo } from 'react';\n\nexport type HtmlBlockProps = {\n html: string;\n};\n\n/**\n * A message rendered as sanitized HTML — the email case, which is not markdown and cannot be a\n * CodeMirror document.\n *\n * Its presence is the reason the item renderer is generalized rather than markdown-only: the\n * engine owns virtualization, chrome and selection anchors, not what a message looks like inside.\n * As plain (non-contenteditable) DOM it is also the control case for cross-item selection.\n */\nexport const HtmlBlock = memo(({ html }: HtmlBlockProps) => {\n const sanitized = useMemo(() => DOMPurify.sanitize(html, { USE_PROFILES: { html: true } }), [html]);\n\n return <div className='dx-feed-html' dangerouslySetInnerHTML={{ __html: sanitized }} />;\n});\n\nHtmlBlock.displayName = 'HtmlBlock';\n","//\n// Copyright 2026 DXOS.org\n//\n\n/** One replacement taking `from` to `to`, as a CodeMirror change spec. */\nexport type RangeChange = { from: number; to: number; insert: string };\n\n/**\n * The smallest single replacement that turns `before` into `after`: the common prefix and suffix\n * are left alone, so a document whose middle changed keeps the decorations either side of it.\n */\nexport const diffRange = (before: string, after: string): RangeChange => {\n const limit = Math.min(before.length, after.length);\n let prefix = 0;\n while (prefix < limit && before.charCodeAt(prefix) === after.charCodeAt(prefix)) {\n prefix++;\n }\n\n // Bounded by the prefix, so a repeated run is never counted on both sides.\n let suffix = 0;\n while (\n suffix < limit - prefix &&\n before.charCodeAt(before.length - 1 - suffix) === after.charCodeAt(after.length - 1 - suffix)\n ) {\n suffix++;\n }\n\n return { from: prefix, to: before.length - suffix, insert: after.slice(prefix, after.length - suffix) };\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Extension } from '@codemirror/state';\nimport { EditorView } from '@codemirror/view';\n\nimport {\n type ObjectLinkProps,\n type WidgetDef,\n WidgetHostOptions,\n type XmlWidgetRegistry,\n createBasicExtensions,\n createMarkdownExtensions,\n createThemeExtensions,\n decorateMarkdown,\n extendedMarkdown,\n objectLinks,\n widgetHost,\n xmlBlockDecoration,\n xmlFormatting,\n xmlTags,\n} from '@dxos/ui-editor';\n\nimport { highlights, highlightTheme } from './highlight.ts';\n\nexport type ItemExtensionOptions = {\n registry?: XmlWidgetRegistry;\n /** The block widget for `![label](echo://…)` — an object embedded as a card; none by default. */\n objectImage?: WidgetDef<ObjectLinkProps>;\n editable?: boolean;\n themeMode?: 'light' | 'dark';\n setWidgets?: WidgetHostOptions['setWidgets'];\n};\n\n/**\n * The extension set one item's document is built from.\n *\n * Everything that does not depend on the item is **built once and shared**, and that is the point of\n * this module rather than a tidiness. `EditorView.theme()` mints a new `StyleModule` and a new\n * generated class on every call, so a set built per item injects one stylesheet per row and\n * invalidates the whole document's style each time it does. Measured at ~14ms per row against ~2ms\n * for the identical set shared (`baseline/mount`) — six times the cost of the first fill, for\n * stylesheets that are all the same.\n *\n * Extensions are descriptors, not instances: a `StateField` or `ViewPlugin` in a shared array is\n * still instantiated per view, so sharing changes nothing but the identity CodeMirror dedupes on.\n */\nexport const createBlockExtensions = ({\n registry,\n objectImage,\n editable = false,\n themeMode = 'light',\n setWidgets,\n}: ItemExtensionOptions = {}): Extension[] => [\n ...sharedExtensions(registry, editable, themeMode),\n // The one part that cannot be shared: the callback that hands this item's widgets back to it.\n ...(registry\n ? [widgetHost({ setWidgets, bookmarks: ['prompt'] }), xmlTags({ registry }), objectLinks({ image: objectImage })]\n : []),\n];\n\n/** Registries are compared by identity, so a feed's single registry is a single cache scope. */\nconst NO_REGISTRY = {};\n\nconst cache = new WeakMap<object, Map<string, Extension[]>>();\n\nconst sharedExtensions = (\n registry: XmlWidgetRegistry | undefined,\n editable: boolean,\n themeMode: 'light' | 'dark',\n): Extension[] => {\n const scope = registry ?? NO_REGISTRY;\n let byVariant = cache.get(scope);\n if (!byVariant) {\n byVariant = new Map();\n cache.set(scope, byVariant);\n }\n\n const key = `${editable}:${themeMode}`;\n let extensions = byVariant.get(key);\n if (!extensions) {\n extensions = build(registry, editable, themeMode);\n byVariant.set(key, extensions);\n }\n\n return extensions;\n};\n\nconst build = (registry: XmlWidgetRegistry | undefined, editable: boolean, themeMode: 'light' | 'dark'): Extension[] =>\n [\n createBasicExtensions({ readOnly: !editable, editable, lineWrapping: true }),\n // Colours fenced code, as the markdown editor and the previous chat stream do.\n createThemeExtensions({ themeMode, syntaxHighlighting: true }),\n // A registry changes how the document is *parsed*, not only how it is decorated: registered\n // tags have to survive as single blocks through the markdown parser before `xmlTags` can\n // replace them, and without that they render as the literal angle brackets they are.\n registry ? extendedMarkdown({ registry }) : createMarkdownExtensions(),\n // `prompt` is skipped only when its block decoration frames it; otherwise its tags are text like any other.\n registry && xmlFormatting({ skip: registry.prompt ? ['prompt'] : [] }),\n decorateMarkdown(),\n // The tags are hidden but the prompt is NOT framed here: the frame is chrome's, which also\n // owns the rewind toolbar under it. Styling it in both places drew the border twice.\n registry?.prompt && xmlBlockDecoration({ tag: 'prompt', hideTags: true }),\n highlights,\n highlightTheme,\n blockScrollerTheme,\n ].filter(Boolean) as Extension[];\n\n/**\n * An item's editor never scrolls on the block axis: it is auto-height and the feed around it is\n * what scrolls, so its content fits by construction.\n *\n * Left to `auto`, that invariant is broken transiently by any widget that changes height — a tool\n * block's disclosure animating open grows the content every frame while `requestMeasure` lands the\n * editor's new height a frame later, so the scroller is a frame short throughout and paints a\n * scrollbar for the length of the animation. The inline axis keeps `auto`; a wide payload still\n * scrolls.\n */\nconst blockScrollerTheme = EditorView.theme({\n '.cm-scroller': {\n overflowY: 'hidden',\n },\n});\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { EditorState, type Extension } from '@codemirror/state';\nimport { EditorView } from '@codemirror/view';\nimport React, { memo, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport type * as Util from '@dxos/react-ui/Util';\nimport { type ObjectLinkProps, type WidgetDef, type WidgetState, type XmlWidgetRegistry } from '@dxos/ui-editor';\nimport { mx } from '@dxos/ui-theme';\n\nimport { diffRange } from './diff-range.ts';\nimport { createBlockExtensions } from './extensions.ts';\nimport { type HighlightRange, setHighlights } from './highlight.ts';\nimport { useSelectionGroup } from './selection-group.ts';\n\nexport type MarkdownBlockProps = Util.ThemedClassName<{\n text: string;\n /**\n * Drip appended text in per frame (the typewriter) instead of dispatching whole deltas. The\n * source's chunk size is whatever the transport delivered — often a whole block — and painting\n * that in one frame reads as blocks appearing fully formed rather than an answer being written.\n * When it turns off (the turn ended), whatever is still queued lands at once.\n */\n stream?: boolean;\n /**\n * When false the view mounts with `EditorView.editable.of(false)`, which drops `contenteditable`\n * from the DOM. Browsers refuse to extend one selection across two contenteditable hosts, so this\n * is what decides whether a drag can span adjacent items.\n */\n editable?: boolean;\n registry?: XmlWidgetRegistry;\n /** The block widget for an object embedded as a card (`![label](echo://…)`). */\n objectImage?: WidgetDef<ObjectLinkProps>;\n hits?: readonly HighlightRange[];\n /** Number of block widgets this item currently has mounted; 0 once it unmounts. */\n onWidgetsChange?: (count: number) => void;\n}>;\n\n/**\n * One message as its own markdown document.\n *\n * Each item owns a document, so streaming appends and per-message editing stay local — where a\n * single thread-wide document needs a cursor and a range table to know which message it is touching.\n */\nexport const MarkdownBlock = memo(\n ({\n classNames,\n text,\n stream,\n editable = false,\n registry,\n objectImage,\n hits,\n onWidgetsChange,\n }: MarkdownBlockProps) => {\n const themeMode = Hooks.useThemeMode();\n const [view, setView] = useState<EditorView | null>(null);\n // React widgets render in portals into hosts the extension places in the document, so the item has\n // to own them: a widget's tree belongs to the React root that rendered the item, not to CodeMirror.\n const [widgets, setWidgets] = useState<WidgetState[]>([]);\n const rootRef = useRef<HTMLDivElement>(null);\n\n // Read through a ref so the group never lands in the extension deps: rebuilding extensions\n // reconstructs the view, which would discard the selection the reader just made.\n const selectionGroup = useSelectionGroup();\n const selectionGroupRef = useRef(selectionGroup);\n selectionGroupRef.current = selectionGroup;\n\n const extensions = useMemo<Extension[]>(\n () => [\n ...createBlockExtensions({ registry, objectImage, editable, themeMode, setWidgets }),\n EditorView.updateListener.of((update) => {\n if (update.selectionSet && !update.state.selection.main.empty) {\n selectionGroupRef.current.claim(update.view);\n }\n }),\n ],\n [editable, themeMode, registry, objectImage],\n );\n\n // Deliberately NOT `useTextEditor`, which builds the view in a passive effect — i.e. after paint.\n // The virtualizer measures each row as it mounts, so a row whose document does not exist yet\n // measures at chrome height and then grows, shifting every row below it. Constructing the view in\n // the layout phase gives the row its real height before the first paint, so it is measured once.\n const initialTextRef = useRef(text);\n useLayoutEffect(() => {\n const parent = rootRef.current;\n if (!parent) {\n return;\n }\n\n // Built detached and attached once: given a `parent`, the view writes its attributes on a live\n // element, and each write invalidates the document's style for every row that mounts.\n const instance = new EditorView({\n state: EditorState.create({ doc: initialTextRef.current, extensions }),\n });\n parent.appendChild(instance.dom);\n const unregister = selectionGroupRef.current.register(instance);\n setView(instance);\n\n return () => {\n unregister();\n instance.destroy();\n setView(null);\n };\n }, [extensions]);\n\n // The document belongs to the model, not the view: a message whose text changes is reconciled\n // rather than remounted, so the virtualizer's measurement for this row survives the update.\n //\n // Two cursors: `targetRef` is the model's latest text, `dispatchedRef` is what the document\n // holds. Outside streaming they move together; while streaming, the drip below advances\n // `dispatchedRef` a few characters per frame toward the target.\n const targetRef = useRef(text);\n const dispatchedRef = useRef(text);\n const dripRaf = useRef(0);\n\n useEffect(() => {\n if (!view) {\n return;\n }\n\n targetRef.current = text;\n initialTextRef.current = text;\n if (dispatchedRef.current === text) {\n return;\n }\n\n // A non-append (a rewrite, a view-type switch, a status tag changing) lands atomically —\n // dripping a replacement would show the reader a half-rewritten document — and replaces only\n // the span that differs, so the decorations and widgets either side of it survive.\n if (!text.startsWith(dispatchedRef.current)) {\n cancelAnimationFrame(dripRaf.current);\n dripRaf.current = 0;\n dispatchedRef.current = text;\n view.dispatch({ changes: diffRange(view.state.doc.toString(), text) });\n return;\n }\n\n if (!stream) {\n // The turn is over (or was never streaming): whatever is pending lands at once.\n cancelAnimationFrame(dripRaf.current);\n dripRaf.current = 0;\n const from = dispatchedRef.current.length;\n dispatchedRef.current = text;\n view.dispatch({ changes: { from, insert: text.slice(from) } });\n return;\n }\n\n // The typewriter: a bounded slice of the backlog per frame, so the cadence is a write rather\n // than a paste and a burst still catches up within a second. XML tags land whole — a dripped\n // half-tag would flash literal markup before the parser can classify it.\n if (dripRaf.current) {\n return;\n }\n\n const tick = () => {\n const target = targetRef.current;\n const dispatched = dispatchedRef.current;\n if (!target.startsWith(dispatched)) {\n // The target was rewritten mid-drip; the effect above handles it on its next run.\n dripRaf.current = 0;\n return;\n }\n\n const backlog = target.slice(dispatched.length);\n if (!backlog.length) {\n dripRaf.current = 0;\n return;\n }\n\n let take = Math.max(2, Math.ceil(backlog.length / 30));\n if (backlog[0] === '<') {\n const close = backlog.indexOf('>');\n take = close === -1 ? 0 : Math.max(take, close + 1);\n } else {\n const tag = backlog.indexOf('<');\n if (tag !== -1) {\n take = Math.min(take, tag);\n }\n }\n\n if (take > 0) {\n const insert = backlog.slice(0, take);\n dispatchedRef.current = dispatched + insert;\n view.dispatch({ changes: { from: dispatched.length, insert } });\n }\n\n dripRaf.current = requestAnimationFrame(tick);\n };\n\n dripRaf.current = requestAnimationFrame(tick);\n }, [view, text, stream]);\n\n // The queue dies with the item; the document is rebuilt from the model on the next mount.\n useEffect(() => () => cancelAnimationFrame(dripRaf.current), []);\n\n // Nothing is dispatched to clear highlights that were never set: a transaction makes CodeMirror\n // re-measure the document, and a viewport of rows mounting is a viewport of forced layouts for a\n // search that is not running.\n const highlighted = useRef(false);\n useEffect(() => {\n if (!view || (!hits?.length && !highlighted.current)) {\n return;\n }\n\n highlighted.current = Boolean(hits?.length);\n view.dispatch({ effects: setHighlights.of(hits ?? []) });\n }, [view, hits]);\n\n // Reported rather than derived: the widgets are portals into the item's own document, so nothing\n // above the item can count them without reaching into the DOM.\n useEffect(() => {\n onWidgetsChange?.(widgets.length);\n return () => onWidgetsChange?.(0);\n }, [widgets.length, onWidgetsChange]);\n\n // A block widget is measured once, when CodeMirror mounts it. Its content is React rendered into a\n // portal, so anything that changes height afterwards — a disclosure opening, an image loading —\n // leaves the editor holding a stale height and drawing the lines below it over the widget.\n //\n // Observing the portal roots and asking for a re-measure is what keeps the document's geometry\n // honest; the row's own height then follows through the virtualizer's observer.\n useEffect(() => {\n if (!view || !widgets.length) {\n return;\n }\n\n const observer = new ResizeObserver(() => view.requestMeasure());\n for (const { root } of widgets) {\n observer.observe(root);\n }\n\n return () => observer.disconnect();\n }, [view, widgets]);\n\n return (\n <>\n {/* No query container here: a prompt's bubble is sized by this element's text, which\n inline-size containment would stop contributing. */}\n <div className={mx(classNames)} ref={rootRef} />\n {widgets.map(({ Component, root, id, props }) => (\n <div key={id}>{createPortal(<Component view={view ?? undefined} {...props} />, root)}</div>\n ))}\n </>\n );\n },\n);\n\nMarkdownBlock.displayName = 'MarkdownBlock';\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type ContentBlock, type Message } from '@dxos/types';\n\n/**\n * What an item renders. The engine is renderer-agnostic: a message may resolve to a markdown\n * document, to raw HTML (email bodies, which are not markdown and cannot be a CodeMirror document),\n * or to a caller-supplied component.\n */\nexport type ItemContent =\n | { kind: 'markdown'; text: string }\n | { kind: 'html'; html: string }\n | { kind: 'custom'; key: string; data?: unknown };\n\n/** Resolves a message to the content its item renders. One per scenario (chat, email, …). */\nexport type MessageRenderer = (message: Message.Message) => ItemContent;\n\n/**\n * Default renderer: an `text/html` block wins (email), otherwise every block is flattened to\n * markdown. Block kinds the engine does not know become fenced JSON so nothing renders as blank.\n */\nexport const defaultRenderer: MessageRenderer = (message) => createContent(message, false);\n\n/**\n * Assistant chat: as {@link defaultRenderer}, plus the reader's own turns marked with `<prompt>`.\n *\n * Separate because the tag is only meaningful where a widget registry knows it — a feed rendered\n * without one would show the reader their own angle brackets, which is what a human chat, a\n * transcript and a comment list all are.\n */\nexport const chatRenderer: MessageRenderer = (message) => createContent(message, true);\n\nconst createContent = (message: Message.Message, prompts: boolean): ItemContent => {\n const textBlocks = message.blocks.filter((block): block is ContentBlock.Text => block._tag === 'text');\n const html = textBlocks.find((block) => block.mimeType === 'text/html');\n if (html) {\n return { kind: 'html', html: html.text };\n }\n\n return {\n kind: 'markdown',\n text: message.blocks\n .map((block) => blockToMarkdown(message, block, prompts))\n .filter(Boolean)\n .join('\\n\\n'),\n };\n};\n\n/**\n * One block as markdown, with anything that is not prose emitted as an XML tag for a widget to\n * render. A message is the concatenation of these, which is what makes an item a document rather\n * than a component tree: the blocks are content, and the chrome around them belongs to the host.\n */\nconst blockToMarkdown = (message: Message.Message, block: ContentBlock.Any, prompts: boolean): string => {\n switch (block._tag) {\n case 'text':\n // A prompt is marked rather than styled: the reader's own words are content, and the widget\n // that frames them is the host's business.\n return prompts && message.sender.role === 'user' ? tag('prompt', block.text, block) : block.text.trim();\n case 'reasoning':\n return tag('reasoning', block.reasoningText ?? block.redactedText ?? '', block);\n case 'status':\n return tag('status', block.statusText, block);\n case 'summary':\n return tag('summary', block.content, block);\n case 'suggestion':\n return tag('suggestion', block.text, block);\n case 'select':\n return block.options.length\n ? `<select>${block.options.map((option) => `<option>${escapeXml(option)}</option>`).join('')}</select>`\n : '';\n case 'toolCall':\n // Name and status ride on the tag rather than in widget state kept beside the document: an\n // item is rebuilt from its message alone, so anything the widget needs has to survive there.\n return `<toolCall id=\"${escapeXml(block.toolCallId)}\" name=\"${escapeXml(block.name)}\"${block.pending ? ' pending=\"true\"' : ''} />`;\n case 'toolResult':\n return tag('toolResult', String(block.result ?? block.error ?? ''), block, `for=\"${escapeXml(block.name)}\"`);\n default:\n // Nothing renders blank: an unknown block is shown as what it is.\n return tag('json', JSON.stringify(block), block);\n }\n};\n\n/**\n * An XML tag holding block content.\n *\n * A pending block is emitted **unclosed**, which is the streaming contract: the item reconciles by\n * appending, so a closing tag written before the content is complete would have to be rewritten on\n * every chunk — and rewriting the document is what discards decorations and scroll position.\n *\n * Paragraph breaks inside the content are collapsed, since a blank line would end the enclosing\n * markdown block and leave the rest of the tag parsing as prose.\n */\nconst tag = (name: string, content: string, block: ContentBlock.Any, attributes?: string): string => {\n const text = escapeXml(content.replace(/\\n\\n+/g, ' ').trim());\n if (!text.length && !block.pending) {\n return '';\n }\n\n const open = `<${name}${attributes ? ` ${attributes}` : ''}>`;\n return block.pending ? `${open}${text}` : `${open}${text}</${name}>`;\n};\n\nconst escapeXml = (raw: string): string => raw.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n\n/**\n * Whether a message is the reader's own prompt. Role alone cannot say: tool results travel back as\n * `user`-role messages (the AI protocol convention), and a synthetic turn is system-generated input\n * — neither is the reader speaking, so neither gets the prompt frame or counts as a stop.\n */\nexport const isPrompt = (message: Message.Message): boolean =>\n message.sender.role === 'user' &&\n message.blocks.some((block) => block._tag === 'text' && block.disposition !== 'synthetic');\n\n/** Plain text of a message, used by search and copy so both read the same projection. */\nexport const messageText = (message: Message.Message, renderer: MessageRenderer): string => {\n const content = renderer(message);\n switch (content.kind) {\n case 'markdown':\n return content.text;\n case 'html':\n return stripHtml(content.html);\n case 'custom':\n return '';\n }\n};\n\n/** Crude tag strip — the engine only needs searchable/copyable text, not a parse tree. */\nconst stripHtml = (html: string): string =>\n html\n .replace(/<[^>]*>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n\n//\n// Search\n//\n\nexport type SearchHit = {\n messageId: string;\n /** Index into the message list, so the virtualizer can scroll to it without a second lookup. */\n index: number;\n /** Offset into the message's rendered text. */\n offset: number;\n length: number;\n};\n\n/**\n * Search the model rather than the DOM.\n *\n * Virtualization means most messages are not mounted, and a `viewType` filter can hide blocks that\n * are still in the model — neither is reachable by a per-view CodeMirror search, so thread-wide find\n * has to run over the projection.\n */\nexport const searchFeed = (\n messages: readonly Message.Message[],\n renderer: MessageRenderer,\n query: string,\n): SearchHit[] => {\n const hits: SearchHit[] = [];\n if (!query.length) {\n return hits;\n }\n\n const needle = query.toLowerCase();\n messages.forEach((message, index) => {\n const text = messageText(message, renderer).toLowerCase();\n let offset = text.indexOf(needle);\n while (offset !== -1) {\n hits.push({ messageId: message.id, index, offset, length: query.length });\n offset = text.indexOf(needle, offset + needle.length);\n }\n });\n\n return hits;\n};\n\n//\n// Selection\n//\n\n/** A point in the feed: a message plus an offset into its rendered text. */\nexport type FeedAnchor = { messageId: string; offset: number };\n\nexport type FeedRange = { from: FeedAnchor; to: FeedAnchor };\n\n/**\n * Text between two anchors, spanning message boundaries.\n *\n * Native selection cannot span separate editor instances, and unmounted messages are not in the DOM\n * at all, so cross-message copy is reconstructed from the model.\n */\nexport const sliceFeed = (\n messages: readonly Message.Message[],\n renderer: MessageRenderer,\n range: FeedRange,\n): string => {\n const fromIndex = messages.findIndex((message) => message.id === range.from.messageId);\n const toIndex = messages.findIndex((message) => message.id === range.to.messageId);\n if (fromIndex === -1 || toIndex === -1) {\n return '';\n }\n\n // Normalize so a backwards drag (focus before anchor) yields the same text as a forwards one.\n const [start, end] =\n fromIndex < toIndex || (fromIndex === toIndex && range.from.offset <= range.to.offset)\n ? [range.from, range.to]\n : [range.to, range.from];\n\n const [startIndex, endIndex] = fromIndex < toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex];\n\n const parts: string[] = [];\n for (let index = startIndex; index <= endIndex; index++) {\n const text = messageText(messages[index], renderer);\n const from = index === startIndex ? start.offset : 0;\n const to = index === endIndex ? end.offset : text.length;\n parts.push(text.slice(from, to));\n }\n\n return parts.join('\\n\\n');\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { ListModel } from '@dxos/react-ui-virtual';\nimport { type Message } from '@dxos/types';\n\nimport {\n type MessageRenderer,\n type SearchHit,\n defaultRenderer,\n isPrompt,\n messageText,\n searchFeed,\n} from './feed-model.ts';\n\n/**\n * One position navigation can land on (SPEC: Stop).\n *\n * Produced by a pluggable policy over the model and consumed identically by every driver —\n * toolbar, arrow keys, outliner, minimap — which is what keeps the rails and the keymap from\n * growing parallel wiring.\n */\nexport type Stop = {\n index: number;\n id: string;\n label?: string;\n};\n\n/** The built-in policies; a host may hand a function instead. */\nexport type StopsPolicy = 'message' | 'prompt' | ((message: Message.Message, index: number) => boolean);\n\nexport type FeedModelOptions = {\n messages?: readonly Message.Message[];\n stops?: StopsPolicy;\n /** Paged history: consumed when the window reaches the start edge. Paging is the model's business. */\n loadBefore?: () => Promise<readonly Message.Message[]>;\n /** The forward twin, for feeds that are windows into something longer. */\n loadAfter?: () => Promise<readonly Message.Message[]>;\n};\n\n/**\n * The native case, not an add-on (SPEC: FeedModel): a ListModel of messages carrying the feed's\n * own semantics — the stops policy, the streaming tail, and iteration. The list consumes this and\n * nothing else; `hits`, `isAnchor` and `streamingId` all left the props API and live here or in\n * the decoration provider.\n *\n * ECHO/queue bindings adapt to this outside the package; `fromMessages` adapts the plain case.\n */\nexport class FeedModel extends ListModel<Message.Message> {\n #stops: StopsPolicy;\n #streamingId: string | undefined;\n #loading = false;\n readonly #loadBefore?: () => Promise<readonly Message.Message[]>;\n readonly #loadAfter?: () => Promise<readonly Message.Message[]>;\n\n constructor({ messages = [], stops = 'message', loadBefore, loadAfter }: FeedModelOptions = {}) {\n super({ items: messages, getId: (message) => message.id });\n this.#stops = stops;\n this.#loadBefore = loadBefore;\n this.#loadAfter = loadAfter;\n }\n\n get messages(): readonly Message.Message[] {\n return this.items;\n }\n\n /** The tail message reconciling by delta; its item must not remount per chunk. */\n get streamingId(): string | undefined {\n return this.#streamingId;\n }\n\n setStreaming(id: string | undefined): void {\n const previous = this.#streamingId;\n if (previous === id) {\n return;\n }\n this.#streamingId = id;\n // The item that stopped (or started) streaming re-renders; the rest of the feed does not care.\n for (const changed of [previous, id]) {\n if (changed) {\n this.update(changed);\n }\n }\n }\n\n /** A chunk arrived for the streaming tail: same identity, longer content. */\n stream(id: string): void {\n this.update(id);\n }\n\n setStops(stops: StopsPolicy): void {\n this.#stops = stops;\n }\n\n /**\n * The positions navigation can land on, under the current policy.\n *\n * Recomputed on demand rather than cached: a policy is a predicate over the model, and the model\n * knows when it changed — callers that want reactivity derive from `rowsAtom`.\n */\n stops(): Stop[] {\n const predicate = this.#stops === 'message' ? () => true : this.#stops === 'prompt' ? isPrompt : this.#stops;\n\n const stops: Stop[] = [];\n this.messages.forEach((message, index) => {\n if (predicate(message, index)) {\n stops.push({ index, id: message.id });\n }\n });\n\n return stops;\n }\n\n /**\n * The window reached the start: ask the source for history.\n *\n * The model owns iteration (SPEC F-7.2) — the pages arrive as a prepend, which the virtualizer\n * is told about, so nothing on screen moves (F-1.1). Serialized, because edges are reached once\n * per frame while the reader sits on them.\n */\n async more(edge: 'start' | 'end'): Promise<boolean> {\n const load = edge === 'start' ? this.#loadBefore : this.#loadAfter;\n if (!load || this.#loading) {\n return false;\n }\n\n this.#loading = true;\n try {\n const page = await load();\n if (!page.length) {\n return false;\n }\n\n if (edge === 'start') {\n this.prepend(page);\n } else {\n this.append(page);\n }\n\n return true;\n } finally {\n this.#loading = false;\n }\n }\n\n /** Model-level search, since virtualization hides most items from any DOM search. */\n search(query: string, renderer: MessageRenderer = defaultRenderer): SearchHit[] {\n return searchFeed(this.messages, renderer, query);\n }\n\n /** The rendered text of one message, for previews and rails. */\n textOf(id: string, renderer: MessageRenderer = defaultRenderer): string | undefined {\n const index = this.indexOf(id);\n const message = this.at(index);\n return message && messageText(message, renderer);\n }\n}\n\n/** The plain case: a feed over an array the host already has. */\nexport const fromMessages = (messages: readonly Message.Message[], options: Omit<FeedModelOptions, 'messages'> = {}) =>\n new FeedModel({ messages, ...options });\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { useEffect, useMemo } from 'react';\n\nimport { type Message } from '@dxos/types';\n\nimport { FeedModel, type FeedModelOptions } from './feed.ts';\n\n/**\n * A feed model over a message array the host re-creates per render — the React-prop case.\n *\n * The model is stable; the array is folded into it with `replace`, which is where the prepend\n * inference lives (and the only place — SPEC F-7.1). In an effect rather than during render: the\n * model publishes synchronously to its subscribers, and a subscriber invalidating itself while the\n * host is still rendering is a cross-component update React rejects. The window it opens is one\n * frame, and the window's own subscription closes it.\n *\n * Streaming through this adapter is a `replace` whose length does not change; the model still\n * publishes, so the window still re-renders, and the streaming item reconciles by document delta\n * exactly as it would under a told update.\n */\nexport const useFeedModel = (\n messages: readonly Message.Message[],\n options: Omit<FeedModelOptions, 'messages'> = {},\n): FeedModel => {\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const model = useMemo(() => new FeedModel({ messages, ...options }), []);\n useEffect(() => {\n if (model.messages !== messages) {\n model.replace(messages);\n }\n }, [model, messages]);\n\n return model;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n/** Movement below this is sub-pixel rounding, not a row changing place. */\nconst EPSILON = 1;\n\nexport type PositionStats = {\n /** Rows whose offset changed after they were laid out — every one of these is a visible flicker. */\n shifts: number;\n /** Windows where offsets did not increase with index; a row was placed over its neighbour. */\n breaks: number;\n /** The most recent shift, for reading in the console while scrolling. */\n last?: PositionShift;\n};\n\nexport type PositionShift = { index: number; from: number; to: number; delta: number };\n\n/** Recent shifts kept for inspection — enough to see whether one row type moves or all of them. */\nconst TRACE_LIMIT = 40;\n\n/** `key` is the virtualizer's item key — the message id, via `getItemKey`. */\nexport type PositionKey = number | string | bigint;\n\nexport type PositionEntry = { key: PositionKey; index: number; start: number };\n\n/** Movement of the viewport itself between samples, which every row is expected to follow. */\nexport type PositionFrame = { scrollOffset: number };\n\nconst EMPTY: PositionStats = { shifts: 0, breaks: 0 };\n\n/**\n * Records where each row was placed, and reports when a row moves.\n *\n * A virtualized list is only still if a row's offset, once laid out, does not change: rows above the\n * reader are measured as they mount, and any correction there pushes everything below it. Scrolling\n * **up** is where this shows, because that is the direction in which unmeasured rows enter — going\n * down, the rows above have already been measured.\n *\n * Positions are recorded **relative to the viewport** (`start - scrollOffset`), not as absolute\n * document offsets, because those two answer different questions. Measuring a row for the first time\n * re-lays every row below it, and the virtualizer compensates by moving `scrollTop` the same\n * distance — the offsets all change and the reader sees nothing. What a reader sees is a row landing\n * somewhere else on screen, which is what this counts.\n *\n * Two invariants:\n *\n * 1. a row keyed by message id holds its place on screen for as long as it stays mounted;\n * 2. positions within the mounted window increase strictly with index.\n *\n * The count is what a reader means by \"flicker\". Deliberately kept as a count rather than a log:\n * a scroll produces thousands of samples, and the number is the signal.\n */\n/**\n * Watches for jumps in what was actually painted, once per animation frame.\n *\n * The counting above is done per React render, which cannot separate a row moving from the sampling\n * happening at a different moment than the layout. This reads the scroll and every row's box in the\n * **same** frame and compares each row's travel against the scroll's: a row that keeps its place in\n * the document travels exactly as far as the scroll did, so anything else is the reader seeing a\n * jump. It is the in-page form of measuring a screen recording frame by frame, and it is what a\n * developer should have running while working on the list.\n */\nexport const useJumpDetector = (viewport: HTMLElement | null, enabled = true) => {\n const [jumps, setJumps] = useState({ count: 0, worst: 0 });\n\n useEffect(() => {\n if (!viewport || !enabled) {\n return;\n }\n\n let handle = 0;\n let previous = new Map<string, number>();\n let previousScroll = viewport.scrollTop;\n let count = 0;\n let worst = 0;\n\n const tick = () => {\n const scroll = viewport.scrollTop;\n const travelled = previousScroll - scroll;\n const current = new Map<string, number>();\n for (const row of viewport.querySelectorAll<HTMLElement>('[data-index]')) {\n const top = row.getBoundingClientRect().top;\n const index = row.dataset.index!;\n current.set(index, top);\n const before = previous.get(index);\n if (before !== undefined) {\n const residual = Math.abs(top - (before + travelled));\n if (residual > EPSILON) {\n count++;\n worst = Math.max(worst, Math.round(residual));\n }\n }\n }\n\n previous = current;\n previousScroll = scroll;\n handle = requestAnimationFrame(tick);\n };\n\n handle = requestAnimationFrame(tick);\n const timer = setInterval(() => setJumps({ count, worst }), 250);\n return () => {\n cancelAnimationFrame(handle);\n clearInterval(timer);\n };\n }, [viewport, enabled]);\n\n return jumps;\n};\n\nexport const usePositionLog = () => {\n const positions = useRef(new Map<PositionKey, number>());\n const trace = useRef<PositionShift[]>([]);\n const [stats, setStats] = useState<PositionStats>(EMPTY);\n const pending = useRef<PositionStats>(EMPTY);\n\n const reset = useCallback(() => {\n positions.current.clear();\n trace.current = [];\n pending.current = EMPTY;\n setStats(EMPTY);\n }, []);\n\n const record = useCallback((entries: readonly PositionEntry[], { scrollOffset }: PositionFrame) => {\n let { shifts, breaks, last } = pending.current;\n\n let previousStart = -Infinity;\n for (const { key, index, start: absolute } of entries) {\n const start = absolute - scrollOffset;\n const recorded = positions.current.get(key);\n if (recorded !== undefined && Math.abs(recorded - start) > EPSILON) {\n shifts++;\n last = { index, from: Math.round(recorded), to: Math.round(start), delta: Math.round(start - recorded) };\n trace.current.push(last);\n if (trace.current.length > TRACE_LIMIT) {\n trace.current.shift();\n }\n }\n\n positions.current.set(key, start);\n if (start < previousStart) {\n breaks++;\n }\n previousStart = start;\n }\n\n // Rows that are no longer mounted are forgotten: their next measurement is a fresh layout, and\n // holding the old offset would report every remount as a shift.\n const mounted = new Set(entries.map(({ key }) => key));\n for (const key of positions.current.keys()) {\n if (!mounted.has(key)) {\n positions.current.delete(key);\n }\n }\n\n const next = { shifts, breaks, last };\n pending.current = next;\n setStats((previous) =>\n previous.shifts === next.shifts && previous.breaks === next.breaks ? previous : { ...next },\n );\n }, []);\n\n return { ...stats, trace, record, reset };\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, {\n type ComponentProps,\n type ComponentType,\n type PropsWithChildren,\n type ReactNode,\n type Ref,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { type WindowController, type WindowState, useFollow, useWindow, windowRowProps } from '@dxos/react-ui-virtual';\nimport * as Button from '@dxos/react-ui/Button';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as ScrollArea from '@dxos/react-ui/ScrollArea';\nimport * as Util from '@dxos/react-ui/Util';\nimport { type Message } from '@dxos/types';\nimport { type ObjectLinkProps, type WidgetDef, type XmlWidgetRegistry } from '@dxos/ui-editor';\n\nimport { type FeedNavigation, useDecorations, useFeedNavigation, useItemSelectionValue } from '../../hooks/index.ts';\nimport { type FeedModel, type ItemContent, type MessageRenderer, defaultRenderer } from '../../model/index.ts';\nimport {\n type HighlightRange,\n HtmlBlock,\n MarkdownBlock,\n SelectionGroupContext,\n WidgetScopeProvider,\n WidgetStateProvider,\n createSelectionGroup,\n createWidgetStateStore,\n} from '../Block/index.ts';\nimport { useJumpDetector, usePositionLog } from './position-log.ts';\n\n//\n// Context\n//\n\ntype ScrollAreaRootProps = ComponentProps<typeof ScrollArea.Root>;\n\nconst MESSAGE_LIST_NAME = 'MessageList';\n\n/** Per-message chrome (avatar, timestamp, fork/rewind/reply controls), supplied by the host. */\nexport type MessageChromeProps = PropsWithChildren<{\n message: Message.Message;\n index: number;\n selected: boolean;\n onSelect: (id: string, additive: boolean) => void;\n}>;\n\nexport type MessageRange = { startIndex: number; endIndex: number };\n\n/**\n * The imperative handle a host outside the feed's context drives the list through — the successor\n * of a document controller, shrunk to what a message-indexed feed needs. Everything else (range,\n * census, debug) is context, read via {@link useMessageList}.\n */\nexport type MessageListController = {\n model: FeedModel;\n scrollToBottom: (options?: ScrollToOptions) => void;\n scrollToIndex: (index: number, options?: ScrollToOptions) => void;\n /** The one seam every navigation driver shares: toolbar, arrows, rails (SPEC F-3.2). */\n navigation: FeedNavigation;\n};\n\ntype MessageListContextValue = {\n /** The feed's model: messages, identity, stops, streaming, iteration. The one source (SPEC F-7). */\n model: FeedModel;\n renderer: MessageRenderer;\n registry?: XmlWidgetRegistry;\n /** The block widget for an object embedded as a card (`![label](echo://…)`); the host supplies one that can render a card. */\n objectImage?: WidgetDef<ObjectLinkProps>;\n Chrome: ComponentType<MessageChromeProps>;\n Custom?: ComponentType<{ content: ItemContent & { kind: 'custom' }; message: Message.Message }>;\n debug?: boolean;\n /** The mounted window; `undefined` until the viewport has measured. */\n range?: MessageRange;\n /** Index the reader is on: moved by the arrow keys and by any navigation, tracked while scrolling. */\n currentIndex: number;\n /** Whether the reader is resting on the tail — what a scroll-to-bottom affordance hides against. */\n atEnd: boolean;\n /** Whether the list is keeping the tail in view itself; an affordance to return there has no place while it is. */\n following: boolean;\n /** The one seam every navigation driver calls: toolbar, arrows, outline, minimap (SPEC F-3.2). */\n navigation: FeedNavigation;\n /** Rows mounted right now: the window the reader is paying for. */\n mountedRows: number;\n /** Block widgets mounted across every mounted item — what the visible window actually costs. */\n mountedWidgets: number;\n reportWidgets: (id: string, count: number) => void;\n /** Rows that moved on screen against the scroll, counted per animation frame. */\n jumps: { count: number; worst: number };\n /** Rows that moved after being laid out, and windows whose offsets were out of order. */\n shifts: number;\n breaks: number;\n resetShifts: () => void;\n /** The element holding the mounted rows, which is what gets measured. */\n windowRef: React.RefObject<HTMLDivElement | null>;\n /** Absolute position of the first mounted row: what that element is translated by (§7). */\n offset: number;\n /** Extent of the whole document, reserved space included — what the thumb is scaled against. */\n sizerExtent: number;\n first: number;\n last: number;\n setViewport: (viewport: HTMLElement | null) => void;\n /** How many times the model has said this message changed in place. */\n revisionOf: (id: string) => number;\n scrollToIndex: (index: number, options?: ScrollToOptions) => void;\n scrollToBottom: (options?: ScrollToOptions) => void;\n};\n\nexport type ScrollToOptions = {\n /** The alignments the virtualizer honours; centering is a host concern it does not have. */\n align?: 'start' | 'end';\n behavior?: 'auto' | 'smooth';\n};\n\nconst [MessageListProvider, useMessageListContext] = Hooks.createContext<MessageListContextValue>(MESSAGE_LIST_NAME);\n\n/**\n * What an item renders with — split from the list's context because that one changes on every\n * scroll (range, offset, cursor), and a consumer re-renders whenever its context does: every mounted\n * row would be rebuilt per frame of a scroll that moved none of them.\n */\ntype MessageListItemContextValue = Pick<\n MessageListContextValue,\n 'model' | 'renderer' | 'registry' | 'objectImage' | 'Custom' | 'debug' | 'reportWidgets'\n>;\n\nconst [MessageListItemProvider, useMessageListItemContext] =\n Hooks.createContext<MessageListItemContextValue>(MESSAGE_LIST_NAME);\n\n/**\n * The list's state and scroll controls, for parts that live outside the viewport — a toolbar's\n * find-next, a statusbar's range readout.\n */\nexport const useMessageList = (consumerName = 'useMessageList') => {\n const {\n range,\n currentIndex,\n atEnd,\n following,\n navigation,\n mountedRows,\n mountedWidgets,\n jumps,\n shifts,\n breaks,\n resetShifts,\n model,\n scrollToIndex,\n scrollToBottom,\n } = useMessageListContext(consumerName);\n return {\n range,\n currentIndex,\n atEnd,\n following,\n navigation,\n mountedRows,\n mountedWidgets,\n jumps,\n shifts,\n breaks,\n resetShifts,\n model,\n count: model.count,\n scrollToIndex,\n scrollToBottom,\n };\n};\n\n//\n// Root\n//\n\nexport type MessageListRootProps = PropsWithChildren<{\n /**\n * The feed's model (SPEC F-7): messages and identity, the stops policy, the streaming tail, and\n * iteration (`loadBefore`/`loadAfter`, consumed when the window reaches an edge). `fromMessages`\n * adapts a plain array; ECHO/queue bindings adapt outside this package.\n *\n * What is NOT here, deliberately: search hits (decorations — `DecorationsProvider` + an item's\n * `useDecorations`), selection (`ItemSelectionProvider` + `useItemSelection`), `streamingId` and\n * `isAnchor` (model concerns). The list neither knows nor routes cross-cutting per-item data.\n */\n model: FeedModel;\n renderer?: MessageRenderer;\n registry?: XmlWidgetRegistry;\n /** The block widget for an object embedded as a card (`![label](echo://…)`). */\n objectImage?: WidgetDef<ObjectLinkProps>;\n /**\n * Chrome wrapper; receives the item as `children`. Defaults to a bare frame.\n * Chrome must be layout-stable: a control that changes a row's height on hover or focus\n * re-triggers measurement, and a pointer travelling down the list mid-scroll then shifts every\n * row below it. Toggle such affordances with opacity, or take them out of flow.\n */\n Chrome?: ComponentType<MessageChromeProps>;\n /**\n * Renders a `custom` item. The engine's own kinds are documents — markdown in CodeMirror, HTML —\n * and both build their content asynchronously enough to be measured wrong once. A host component\n * whose height is settled at first paint is the control case: if a feed of those still moves, the\n * fault is the list; if it does not, the fault is in what the item is building.\n */\n Custom?: ComponentType<{ content: ItemContent & { kind: 'custom' }; message: Message.Message }>;\n /**\n * Outline each item and the blocks inside it, so what the layout is measuring is visible.\n */\n debug?: boolean;\n /**\n * Estimated row height before measurement. A function is worth supplying whenever the feed's\n * rows differ widely — a one-line prompt beside a long answer — because a single number is wrong\n * for both.\n */\n estimateSize?: number | ((message: Message.Message, index: number) => number);\n /**\n * Pin to the bottom as messages arrive, and as a streaming message grows (chat behaviour).\n */\n stickyBottom?: boolean;\n /**\n * Blank lines kept below the last row, part of the resting view: the tail sits that much clear\n * of the viewport's end (breathing room above a composer). A constant number in the sizer —\n * lines × the viewport's line-height — not a mode the list has (§7).\n */\n tailLines?: number;\n overscan?: number;\n onRangeChange?: (range: MessageRange) => void;\n /** Published once the list is wired; for hosts composing chrome outside the feed's context. */\n controllerRef?: Ref<MessageListController>;\n}>;\n\nconst DefaultChrome = ({ children }: MessageChromeProps) => <>{children}</>;\n\n/** Row height assumed before anything has been measured. */\nconst DEFAULT_ESTIMATE = 120;\n\n/**\n * Headless root of a feed: owns the placement, the selection group and the model-to-item mapping.\n * Renders no DOM of its own, so a toolbar or statusbar that reads its state can sit outside the\n * scroll container — the scrolling part is `MessageList.Viewport`.\n */\nconst MessageListRoot = ({\n children,\n model,\n renderer = defaultRenderer,\n registry,\n objectImage,\n Chrome = DefaultChrome,\n Custom,\n debug,\n estimateSize = DEFAULT_ESTIMATE,\n stickyBottom = false,\n tailLines = 0,\n overscan = 8,\n onRangeChange,\n controllerRef,\n}: MessageListRootProps) => {\n // Owned here so it outlives every row: a widget's state must survive its row being destroyed.\n const widgetStore = useMemo(createWidgetStateStore, []);\n\n const [viewport, setViewportState] = useState<HTMLElement | null>(null);\n // The same element as a ref, because the placement is bound to an element rather than to a render.\n const scrollerRef = useRef<HTMLElement | null>(null);\n const setViewport = useCallback((element: HTMLElement | null) => {\n scrollerRef.current = element;\n setViewportState(element);\n }, []);\n\n const [range, setRange] = useState<MessageRange | undefined>(undefined);\n const [mounted, setMounted] = useState(0);\n\n // One number standing in for the caller's estimate wherever a single value is needed, since a\n // per-row estimator answers about a row and not about the feed.\n const nominalSize = typeof estimateSize === 'number' ? estimateSize : DEFAULT_ESTIMATE;\n\n const [currentIndex, setCurrentIndex] = useState(0);\n const currentIndexRef = useRef(0);\n const setCurrent = useCallback((index: number) => {\n currentIndexRef.current = index;\n setCurrentIndex(index);\n }, []);\n\n // Widget census, kept per item so an item that unmounts takes its own widgets out of the total.\n // Batched into state rather than counted on every report: a scroll mounts and unmounts several\n // items per frame, and a readout is not worth a render each.\n const widgetCounts = useRef(new Map<string, number>());\n const [mountedWidgets, setMountedWidgets] = useState(0);\n const reportWidgets = useCallback((id: string, count: number) => {\n if (count) {\n widgetCounts.current.set(id, count);\n } else {\n widgetCounts.current.delete(id);\n }\n\n setMountedWidgets([...widgetCounts.current.values()].reduce((total, value) => total + value, 0));\n }, []);\n\n // What the host says, and nothing else.\n //\n // There is no running average here and no re-base built on one. Both existed because the old\n // engine could only revise the rows below the last one it had measured, so a feed opened at its\n // tail kept a document sized by the opening guess and had to be rebuilt from the top to correct\n // it — which moved every offset in the list at once. A measurement is kept against the row's id\n // and consulted from the anchor, so a wrong estimate is wrong only for rows nobody has seen (§8).\n const extents = useMemo(\n () => ({\n of: (index: number) => {\n const message = model.at(index);\n return typeof estimateSize === 'function' && message ? estimateSize(message, index) : nominalSize;\n },\n }),\n [model, estimateSize, nominalSize],\n );\n\n // A constant input to the sizer: lines × the viewport's line-height, read once per mount. The\n // previous design derived a viewport-sized reserve from the tail's measured extents (so the last\n // prompt could reach the top), which was several moving parts — measurements, the stops policy,\n // a freshness freeze — and never reliable. A constant cannot oscillate.\n const [reserve, setReserve] = useState(0);\n useEffect(() => {\n if (!viewport || !tailLines) {\n setReserve(0);\n return;\n }\n\n const lineHeight = parseFloat(getComputedStyle(viewport).lineHeight) || 24;\n setReserve(Math.round(tailLines * lineHeight));\n }, [viewport, tailLines]);\n\n const controller = useRef<WindowController>(null);\n const onWindowChange = useCallback(\n (state: WindowState) => {\n const next = state.count ? { startIndex: state.visible.first, endIndex: state.visible.last } : undefined;\n setRange(next);\n setMounted(state.mounted.last - state.mounted.first + 1);\n if (next) {\n onRangeChange?.(next);\n // The cursor is the row at the top of the viewport, derived from the scroll and never set\n // alongside it. That is what makes a press always move: the row containing the offset is the\n // one the stop above begins strictly higher than, and the stop below strictly lower.\n setCurrent(next.startIndex);\n\n // Iteration is the model's (SPEC F-7.2): the window reaching an edge asks it for more, the\n // page arrives as a prepend the anchor is told about, and nothing on screen moves. The model\n // serializes and drains, so asking every commit costs nothing once the source is done.\n if (next.startIndex === 0) {\n void model.more('start');\n }\n if (next.endIndex >= model.count - 1) {\n void model.more('end');\n }\n }\n },\n [onRangeChange, setCurrent, model],\n );\n\n // The model *is* the window's model: FeedModel satisfies the structural contract, so there is no\n // adapter and no second copy of the truth.\n const { placement, windowRef, offset, sizerExtent, first, last } = useWindow({\n scrollerRef,\n model,\n extents,\n overscan,\n reserve,\n onChange: onWindowChange,\n controllerRef: controller,\n });\n\n // When the content last actually changed, as against merely re-measured: what the follow is\n // allowed to chase (a toggle near the tail grows the document too, and is the reader's business).\n const changedAtRef = useRef(performance.now());\n useEffect(() => model.subscribe(() => (changedAtRef.current = performance.now())), [model]);\n\n // Per-message revision, bumped when the model says a message changed in place: a streamed message\n // keeps its identity, so a memoized row cannot tell it changed from the message alone.\n const revisions = useRef(new Map<string, number>());\n useEffect(\n () =>\n model.subscribe(({ updated }) => {\n for (const id of updated ?? []) {\n revisions.current.set(id, (revisions.current.get(id) ?? 0) + 1);\n }\n }),\n [model],\n );\n const revisionOf = useCallback((id: string) => revisions.current.get(id) ?? 0, []);\n\n // The follow is an aspect the host composes, not part of the window (SPEC §Aspects) — the intent,\n // its withdrawal by a backwards scroll, and the glide all live there.\n const follow = useFollow({\n scrollerRef,\n placement,\n extent: sizerExtent,\n count: model.count,\n reserve,\n enabled: stickyBottom,\n changedAt: () => changedAtRef.current,\n });\n\n // What a reader would call flicker: a row moving on screen by more than the scroll moved, sampled\n // once per frame so the reading is of what was painted. Only while debugging — it reads every\n // mounted row's box every frame.\n const jumps = useJumpDetector(viewport, debug);\n\n // Where each row was placed, and whether it stayed there.\n const { shifts, breaks, trace: shiftTrace, record: recordPositions, reset: resetShifts } = usePositionLog();\n useEffect(() => {\n const rows = [];\n for (let index = first; index <= last; index++) {\n rows.push({ key: model.getId(index), index, start: placement.positionOf(index) });\n }\n\n // The offset the layout was computed against, not the element's current `scrollTop`: reading the\n // DOM here samples a scroll that has moved on since, and every row then looks displaced by\n // exactly one frame of travel.\n recordPositions(rows, { scrollOffset: placement.scroll });\n }, [first, last, offset, sizerExtent, model, placement, recordPositions]);\n\n const scrollToIndex = useCallback(\n (index: number, { align = 'start', behavior = 'auto' }: ScrollToOptions = {}) => {\n // Before the write, not via the scroll event it raises: asking to be somewhere answers \"do\n // you want the tail?\", and the correction effect must not get a word in first.\n follow.onNavigate(index);\n controller.current?.scrollToIndex(index, align, behavior);\n },\n [follow],\n );\n\n const scrollToBottom = useCallback(\n (options?: ScrollToOptions) => {\n if (!model.count) {\n return;\n }\n\n // Already at rest: arm the follow and do nothing else — the jump re-anchors from estimates,\n // which visibly moves a reader who has not moved (seen on submit while pinned).\n const scroller = scrollerRef.current;\n if (scroller && scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop <= 32) {\n follow.onNavigate(model.count - 1);\n return;\n }\n\n // The end includes the reserve: the tail rests its tail-lines clear of the viewport's edge.\n scrollToIndex(model.count - 1, { align: 'end', ...options });\n },\n [scrollToIndex, model, follow],\n );\n\n useEffect(() => {\n if (viewport) {\n // Why the list is where it is, readable from the console as `$0.__feed`.\n (viewport as any).__feed = {\n placement,\n get layout() {\n return placement.layout();\n },\n get shifted() {\n return { shifts, breaks, recent: shiftTrace.current };\n },\n };\n }\n }, [viewport, placement, shifts, breaks, shiftTrace]);\n\n // The one seam every driver calls (SPEC F-3.2): the toolbar's buttons, the arrows below, and the\n // rails all step through this, over the stops the model's policy names (SPEC F-3.1).\n const navigation = useFeedNavigation({\n controller,\n stops: () => model.stops(),\n current: () => currentIndexRef.current,\n count: () => model.count,\n // The follow scrolls only while content arrives AND the reader is pinned to the bottom;\n // navigating anywhere but the last row un-pins, before the jump's own scroll event fires.\n onNavigate: follow.onNavigate,\n });\n\n // Arrow keys move by message, not by line: a plain arrow steps the cursor to the adjacent message\n // and Cmd/Ctrl + Arrow jumps to the first or last.\n //\n // Bound imperatively to the viewport element (rather than as a React prop) because\n // `ScrollArea.Viewport` narrows its props to the slottable set, and wrapping it in a focusable div\n // would insert a box into the height chain.\n useEffect(() => {\n if (!viewport) {\n return;\n }\n\n // The items are non-editable, so nothing inside the list takes focus on its own; the viewport\n // has to be focusable for the keymap to have somewhere to land.\n viewport.tabIndex = 0;\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.altKey || event.shiftKey) {\n return;\n }\n\n const delta = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;\n if (!delta) {\n return;\n }\n\n event.preventDefault();\n if (event.metaKey || event.ctrlKey) {\n if (delta > 0) {\n navigation.last();\n } else {\n navigation.first();\n }\n return;\n }\n\n navigation.step(delta);\n };\n\n viewport.addEventListener('keydown', onKeyDown);\n return () => viewport.removeEventListener('keydown', onKeyDown);\n }, [viewport, navigation]);\n\n // The feed has one selection even though it has many editors; the group is what enforces that.\n const selectionGroup = useMemo(createSelectionGroup, []);\n\n // The parts are stable identities, so this republishes only when the model itself is replaced.\n useEffect(() => {\n Hooks.setRef(controllerRef, { model, scrollToBottom, scrollToIndex, navigation });\n return () => {\n Hooks.setRef(controllerRef, null);\n };\n }, [controllerRef, model, scrollToBottom, scrollToIndex, navigation]);\n\n return (\n <SelectionGroupContext.Provider value={selectionGroup}>\n <WidgetStateProvider store={widgetStore}>\n <MessageListProvider\n model={model}\n renderer={renderer}\n registry={registry}\n objectImage={objectImage}\n Chrome={Chrome}\n Custom={Custom}\n debug={debug}\n range={range}\n currentIndex={currentIndex}\n atEnd={follow.atEnd}\n following={follow.following}\n navigation={navigation}\n mountedRows={mounted}\n mountedWidgets={mountedWidgets}\n reportWidgets={reportWidgets}\n jumps={jumps}\n shifts={shifts}\n breaks={breaks}\n resetShifts={resetShifts}\n windowRef={windowRef}\n offset={offset}\n sizerExtent={sizerExtent}\n first={first}\n last={last}\n setViewport={setViewport}\n revisionOf={revisionOf}\n scrollToIndex={scrollToIndex}\n scrollToBottom={scrollToBottom}\n >\n <MessageListItemProvider\n model={model}\n renderer={renderer}\n registry={registry}\n objectImage={objectImage}\n Custom={Custom}\n debug={debug}\n reportWidgets={reportWidgets}\n >\n {children}\n </MessageListItemProvider>\n </MessageListProvider>\n </WidgetStateProvider>\n </SelectionGroupContext.Provider>\n );\n};\n\nMessageListRoot.displayName = 'MessageList.Root';\n\n//\n// Viewport\n//\n\nconst MESSAGE_LIST_VIEWPORT_NAME = 'MessageList.Viewport';\n\ntype MessageListViewportExtra = Pick<ScrollAreaRootProps, 'autoHide' | 'native' | 'scrollbars'> & {\n /** Inline gutter of each row, as a page Container's. */\n gutter?: 'sm' | 'md' | 'lg';\n /**\n * Chrome pinned over the scroller — a scroll-to-bottom pill, a \"new messages\" badge.\n *\n * Mounted inside `ScrollArea.Root` because it is positioned and does not scroll, so nothing has\n * to enter the flex-height chain the placement measures.\n */\n overlay?: ReactNode;\n};\n\n/**\n * The scroll container and the mounted window of rows.\n *\n * Scrolling belongs to `ScrollArea`, which owns the overlay thumbs and the padding tokens; the\n * placement needs only the element being scrolled, which `ScrollArea.Viewport` publishes.\n */\n/**\n * Whether a message renders to nothing under the current renderer — e.g. a stats-only turn, whose\n * block the renderer deliberately maps to no output. The message stays in the model (identity,\n * stops, search all see it); only its chrome is withheld, so the reader is not shown an empty row\n * with a toolbar.\n */\nconst isEmptyContent = (content: ItemContent, hasCustomRenderer: boolean): boolean =>\n (content.kind === 'markdown' && !content.text.trim()) ||\n (content.kind === 'html' && !content.html.trim()) ||\n (content.kind === 'custom' && !hasCustomRenderer);\n\nconst MessageListViewport = Util.composable<HTMLDivElement, MessageListViewportExtra>(\n ({ autoHide, native, scrollbars, gutter = 'md', overlay, ...props }, forwardedRef) => {\n const { model, renderer, Chrome, Custom, windowRef, offset, sizerExtent, first, last, setViewport, revisionOf } =\n useMessageListContext(MESSAGE_LIST_VIEWPORT_NAME);\n // The value once, per-row state derived: hooks do not run in loops, and the row loop below is\n // one. Item-shaped chrome uses `useItemSelection(id)` instead.\n const { selectedIds, onSelect } = useItemSelectionValue();\n\n const handleViewportRef = useCallback(\n (element: HTMLDivElement | null) => {\n setViewport(element);\n Hooks.setRef(forwardedRef, element);\n },\n [setViewport, forwardedRef],\n );\n\n // Stable, so a memoized row is not re-rendered for a callback that is new only by identity.\n const onSelectRef = useRef(onSelect);\n onSelectRef.current = onSelect;\n const handleSelect = useCallback((id: string, additive: boolean) => onSelectRef.current?.(id, additive), []);\n\n const rows = [];\n for (let index = first; index <= last; index++) {\n const message = model.at(index);\n if (!message) {\n continue;\n }\n\n rows.push(\n <MessageListRow\n key={message.id}\n message={message}\n revision={revisionOf(message.id)}\n index={index}\n // The row div always exists — the measurement pass walks the window's children by index —\n // but an empty render mounts no chrome inside it, so it measures at zero and takes no space.\n empty={isEmptyContent(renderer(message), Boolean(Custom))}\n selected={selectedIds?.has(message.id) ?? false}\n gutter={gutter}\n Chrome={Chrome}\n onSelect={handleSelect}\n />,\n );\n }\n\n return (\n <ScrollArea.Root\n {...Util.composableProps(props)}\n orientation='vertical'\n autoHide={autoHide}\n native={native}\n scrollbars={scrollbars}\n >\n <ScrollArea.Viewport\n data-testid='feed.viewport'\n // Off deliberately: the browser adjusting the scroll as well would be a second party\n // anchoring the same thing, and the defect this design exists to remove is exactly that.\n style={{ position: 'relative', overflowAnchor: 'none' }}\n classNames='dx-document'\n ref={handleViewportRef}\n >\n {/* Holds no rows: it exists only to give the thumb something to measure. */}\n <div style={{ height: sizerExtent }} data-testid='feed.sizer' />\n <div\n ref={windowRef}\n className='absolute top-0 left-0 flex flex-col w-full'\n style={{ transform: `translateY(${offset}px)` }}\n data-testid='feed.window'\n >\n {rows}\n </div>\n </ScrollArea.Viewport>\n {overlay}\n </ScrollArea.Root>\n );\n },\n);\n\nMessageListViewport.displayName = MESSAGE_LIST_VIEWPORT_NAME;\n\n//\n// Row\n//\n\ntype MessageListRowProps = {\n message: Message.Message;\n /** Stands in for the message's content: it changes when the model says the message changed in place. */\n revision: number;\n index: number;\n empty: boolean;\n selected: boolean;\n gutter: MessageListViewportExtra['gutter'];\n Chrome: ComponentType<MessageChromeProps>;\n onSelect: (id: string, additive: boolean) => void;\n};\n\n/**\n * One mounted row. Memoized because the window re-renders on every scroll event and after every\n * measurement, and nothing in a row depends on where the window is — only on its own props.\n */\nconst MessageListRow = memo(({ message, index, empty, selected, gutter, Chrome, onSelect }: MessageListRowProps) => (\n // Unpositioned, and that is the point. A row that changes extent reflows the ones after it,\n // in the browser, in the same frame; placing each row ourselves meant re-placing every row\n // below it on every frame of the change — 177 re-placements for one disclosure opening (§6).\n <div data-object-id={message.id} {...windowRowProps(index, message.id)}>\n {!empty && (\n <Layout.Container gutter={gutter}>\n {/* The widgets' query container: it must be an element whose width is definite, since\n containment stops a descendant's content sizing it (a prompt's bubble collapses). */}\n <div className='dx-container-type-inline-size'>\n <Chrome message={message} index={index} selected={selected} onSelect={onSelect}>\n <MessageListItem message={message} />\n </Chrome>\n </div>\n </Layout.Container>\n )}\n </div>\n));\n\nMessageListRow.displayName = 'MessageList.Row';\n\n//\n// Item\n//\n\nconst MESSAGE_LIST_ITEM_NAME = 'MessageList.Item';\n\ntype MessageListItemExtra = {\n message: Message.Message;\n};\n\n/**\n * One message, rendered by the kind its renderer resolves. Exposed so a host can render a message\n * outside the scrolling window — a pinned message, a preview — through the same path.\n */\nconst MessageListItem = Util.composable<HTMLDivElement, MessageListItemExtra>(({ message, ...props }, forwardedRef) => {\n const { model, renderer, registry, objectImage, Custom, debug, reportWidgets } =\n useMessageListItemContext(MESSAGE_LIST_ITEM_NAME);\n const content = renderer(message);\n // The item asks for its own cross-cutting data by id (SPEC §Aspects); the list never routed it.\n const decorations = useDecorations(message.id);\n const hits = useMemo<HighlightRange[] | undefined>(\n () =>\n decorations.length\n ? decorations.map(({ range }) => [range.offset, range.offset + range.length] as HighlightRange)\n : undefined,\n [decorations],\n );\n const handleWidgetsChange = useCallback(\n (count: number) => reportWidgets(message.id, count),\n [reportWidgets, message.id],\n );\n\n return (\n // The outlines are the item and the block-level children of its document — which is exactly what\n // the virtualizer measures and what a widget's late paint changes.\n <div\n {...Util.composableProps(props, {\n classNames: debug\n ? 'outline outline-1 outline-dashed outline-primary-500/50 [&_.cm-content>*]:outline [&_.cm-content>*]:outline-1 [&_.cm-content>*]:outline-dashed [&_.cm-content>*]:outline-neutral-500/40'\n : undefined,\n })}\n ref={forwardedRef}\n >\n {content.kind === 'markdown' && (\n <WidgetScopeProvider scope={message.id}>\n <MarkdownBlock\n text={content.text}\n // The typewriter runs only for the tail the model says is streaming; every other\n // change (an edit, a view switch) lands atomically.\n stream={model.streamingId === message.id}\n registry={registry}\n objectImage={objectImage}\n hits={hits}\n onWidgetsChange={handleWidgetsChange}\n />\n </WidgetScopeProvider>\n )}\n {content.kind === 'html' && <HtmlBlock html={content.html} />}\n {content.kind === 'custom' && Custom && <Custom content={content} message={message} />}\n </div>\n );\n});\n\nMessageListItem.displayName = MESSAGE_LIST_ITEM_NAME;\n\n//\n// Nav\n//\n\nconst MESSAGE_LIST_NAV_NAME = 'MessageList.Nav';\n\ntype MessageListNavExtra = {\n /** Render the jump-to-ends buttons as well as the steppers. @default true */\n ends?: boolean;\n};\n\n/**\n * The step and jump controls, on the same seam as the arrow keys and the rails (SPEC F-3.2), so a\n * reader who switches between them does not change places. Lives outside the viewport — Root is\n * headless precisely so chrome like this can sit in a toolbar.\n *\n * While any of its buttons has focus, ArrowUp/Down step and Meta(Ctrl)+Arrow jumps to the ends —\n * the same keymap the viewport itself carries, so focus landing on the toolbar does not change\n * what the keys mean.\n */\nconst MessageListNav = Util.composable<HTMLDivElement, MessageListNavExtra>(\n ({ ends = true, ...props }, forwardedRef) => {\n const { navigation } = useMessageListContext(MESSAGE_LIST_NAV_NAME);\n\n const onKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n const delta = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;\n if (!delta || event.altKey || event.shiftKey) {\n return;\n }\n\n event.preventDefault();\n if (event.metaKey || event.ctrlKey) {\n if (delta > 0) {\n navigation.last();\n } else {\n navigation.first();\n }\n return;\n }\n\n navigation.step(delta);\n },\n [navigation],\n );\n\n return (\n <div role='group' {...Util.composableProps(props)} onKeyDown={onKeyDown} ref={forwardedRef}>\n {ends && (\n <Button.Root\n icon='ph--arrow-line-up--regular'\n iconOnly\n label='First message'\n variant='ghost'\n data-testid='feed.nav.top'\n onClick={() => navigation.first()}\n />\n )}\n <Button.Root\n icon='ph--caret-up--regular'\n iconOnly\n label='Previous message'\n variant='ghost'\n data-testid='feed.nav.back'\n onClick={() => navigation.step(-1)}\n />\n <Button.Root\n icon='ph--caret-down--regular'\n iconOnly\n label='Next message'\n variant='ghost'\n data-testid='feed.nav.forward'\n onClick={() => navigation.step(1)}\n />\n {ends && (\n <Button.Root\n icon='ph--arrow-line-down--regular'\n iconOnly\n label='Last message'\n variant='ghost'\n data-testid='feed.nav.bottom'\n onClick={() => navigation.last()}\n />\n )}\n </div>\n );\n },\n);\n\nMessageListNav.displayName = MESSAGE_LIST_NAV_NAME;\n\n//\n// MessageList\n//\n\nexport const MessageList = {\n Root: MessageListRoot,\n Viewport: MessageListViewport,\n Item: MessageListItem,\n Nav: MessageListNav,\n};\n\nexport type {\n MessageListItemExtra as MessageListItemProps,\n MessageListNavExtra as MessageListNavProps,\n MessageListViewportExtra as MessageListViewportProps,\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React from 'react';\n\nimport { type WindowState } from '@dxos/react-ui-virtual';\nimport type * as Util from '@dxos/react-ui/Util';\nimport { mx } from '@dxos/ui-theme';\n\n/** 2rem, matching the outline rail it usually sits opposite. */\nconst DEFAULT_WIDTH = 32;\n\n/**\n * The whole list at a glance: what exists, what is mounted, and what the reader can see.\n *\n * Drawn in **content space**, not in indices. The three quantities a virtualized list can get wrong\n * are where the mounted rows are, how big the reader's slice of them is, and whether the total the\n * thumb is scaled against is honest — and none of those are visible from a row count. Here they are\n * three bars on one track, so a mounted range that has drifted away from the viewport, or a total\n * that jumps while nothing scrolls, is a picture rather than a number to be noticed.\n *\n * Debug only. It reads the state the window publishes and draws it (§2); it does not participate.\n */\nexport type MinimapProps = Util.ThemedClassName<{\n state?: WindowState;\n /** Width of the rail, in px. @default 32 (2rem) */\n width?: number;\n /**\n * Jump to where the reader pointed, as a fraction of the whole list.\n *\n * A fraction and not an index: the map is drawn in content space, so what a click names is a\n * position, and turning that into a row is the host's business — it has the count.\n */\n onSelect?: (fraction: number) => void;\n}>;\n\nexport const Minimap = ({ classNames, state, width = DEFAULT_WIDTH, onSelect }: MinimapProps) => {\n const total = state?.geometry.total ?? 0;\n const scale = (value: number) => (total > 0 ? `${Math.max(0, Math.min(100, (value / total) * 100))}%` : '0%');\n\n return (\n <div\n className={mx('relative h-full bg-input-surface overflow-hidden', onSelect && 'cursor-pointer', classNames)}\n style={{ width }}\n title={total ? `${Math.round(total)}px over ${state?.count} rows` : 'empty'}\n data-testid='minimap'\n onClick={\n onSelect &&\n ((event) => {\n const box = event.currentTarget.getBoundingClientRect();\n onSelect(Math.max(0, Math.min(1, (event.clientY - box.top) / box.height)));\n })\n }\n >\n {state && total > 0 && (\n <>\n {/* Mounted rows: where content actually exists. Should always contain the viewport — if it\n does not, the reader is looking at rows nobody has rendered. */}\n <div\n className='absolute inset-x-0 bg-accent-bg/30'\n style={{ top: scale(state.geometry.window.start), height: scale(state.geometry.window.extent) }}\n data-testid='minimap.mounted'\n />\n {/* Tenths, so a glance says roughly where in the list a bar sits without reading a number. */}\n {Array.from({ length: 9 }, (_, step) => (\n <div\n key={step}\n className='absolute inset-x-0 border-t border-separator/40'\n style={{ top: `${((step + 1) / 10) * 100}%` }}\n />\n ))}\n {/* The viewport, which is the reader. */}\n <div\n className='absolute inset-x-0 border-y border-accent-bg bg-accent-bg/70'\n style={{ top: scale(state.geometry.scroll), height: scale(state.geometry.viewport) }}\n data-testid='minimap.viewport'\n />\n </>\n )}\n </div>\n );\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, {\n type KeyboardEvent as ReactKeyboardEvent,\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as Popover from '@dxos/react-ui/Popover';\nimport type * as Util from '@dxos/react-ui/Util';\nimport { mx } from '@dxos/ui-theme';\n\n// Rest tick width (px) and the wave radius (in rows) over which the hover extension falls off. The\n// peak is the rail's own width: a tick at full extension spans it.\nconst REST_WIDTH = 8;\n/** 2rem, matching the debug minimap it usually sits opposite. */\nconst DEFAULT_WIDTH = 32;\nconst WAVE_SPREAD = 2;\n\n/** Height of one tick, and so the pitch the rail thins its markers to. */\nconst TICK_SIZE = 8;\n\nconst intersects = (range: { from: number; to: number }, visible: { from: number; to: number }): boolean =>\n range.from < visible.to && range.to > visible.from;\n\n/**\n * A single anchor marker within the mapped document.\n * `range` is expressed in the document's own position space (e.g. CodeMirror offsets).\n */\nexport type OutlineMarker = {\n id: string;\n title: string;\n description?: string;\n range: { from: number; to: number };\n};\n\nexport type OutlineProps = Util.ThemedClassName<{\n markers: OutlineMarker[];\n /** Currently-visible document range; markers intersecting it render brighter (\"active\"). */\n visibleRange?: { from: number; to: number };\n /**\n * Height of one tick, in px.\n *\n * Fixed rather than shared out over the rail's height: ticks that stretch to fill make a short\n * document's markers enormous and a long one's hairline, so the same rail reads as a different\n * control depending on what is in it. A fixed pitch means the rail thins to what fits and every\n * tick looks the same. @default 8\n */\n tickSize?: number;\n /** Width of the rail, in px, which is also how far a tick extends at the peak of the wave. @default 32 (2rem) */\n width?: number;\n onSelect?: (marker: OutlineMarker, index: number) => void;\n /**\n * Step one item, in whatever the host counts items in.\n *\n * Not one tick: the rail thins its markers to what fits, so the next tick can be ten items away,\n * and a reader pressing an arrow means \"the next one\" rather than \"the next thing I can see\". The\n * host steps it with the same mechanism its toolbar uses, which is why this is a callback rather\n * than something the rail decides.\n */\n onNavigate?: (delta: number) => void;\n}>;\n\n/**\n * A vertical rail of horizontal ticks, each representing an anchor marker in a\n * scrollable document. The rows tile the full height, so hovering anywhere in the rail activates\n * the nearest tick: it (and its neighbours, with a distance falloff) extends rightward in a wave.\n * A single popover — anchored to the tick the reader is at — shows the\n * marker's title/description. Markers whose range intersects `visibleRange` render at full\n * opacity; the rest are dimmed.\n */\n/** A rendered tick: the marker it points at, and the span of the document it stands for. */\ntype Row = { marker: OutlineMarker; index: number; span: { from: number; to: number } };\n\n/**\n * Evenly spreads `markers` over at most `capacity` rows, keeping the first and last.\n *\n * Each rendered tick then stands for the span up to the next one, so a thinned rail still covers\n * the whole document — a position between two ticks belongs to the one above it, rather than to\n * nothing.\n */\nconst thin = (markers: OutlineMarker[], capacity: number): Row[] => {\n const step = capacity > 1 ? (markers.length - 1) / (capacity - 1) : 0;\n const indexes =\n markers.length <= capacity\n ? markers.map((_, index) => index)\n : Array.from({ length: capacity }, (_, row) => Math.round(row * step));\n\n return indexes.map((index, row) => ({\n marker: markers[index],\n index,\n span: { from: markers[index].range.from, to: markers[indexes[row + 1]]?.range.from ?? markers[index].range.to },\n }));\n};\n\nexport const Outline = ({\n classNames,\n markers,\n visibleRange,\n tickSize = TICK_SIZE,\n width = DEFAULT_WIDTH,\n onSelect,\n onNavigate,\n}: OutlineProps) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const rowsRef = useRef<Array<HTMLButtonElement | null>>([]);\n // Two ways to be *at* a tick, kept apart: the pointer is over one, or the rail has focus and the\n // reader has navigated to one. Conflating them means leaving with the mouse cannot dismiss the\n // card — the keyboard position immediately puts it back.\n const [pointer, setPointer] = useState<number | null>(null);\n const [navigated, setNavigated] = useState<number | null>(null);\n // The rail is bounded by whatever contains it. Measured rather than assumed, because the same\n // component is used both in a sized grid cell and floating over a document, where nothing\n // constrains it and its natural height is the right one.\n const [available, setAvailable] = useState(0);\n useEffect(() => {\n const parent = containerRef.current?.parentElement;\n if (!parent) {\n return;\n }\n\n const observer = new ResizeObserver(([entry]) => setAvailable(entry.contentRect.height));\n observer.observe(parent);\n setAvailable(parent.getBoundingClientRect().height);\n return () => observer.disconnect();\n }, []);\n\n const natural = markers.length * tickSize;\n // Below two rows the measurement is a transient (an unlaid-out parent), not a real constraint.\n const bounded = available >= tickSize * 2;\n const rows = useMemo(\n () => thin(markers, bounded ? Math.min(markers.length, Math.floor(available / tickSize)) : markers.length),\n [markers, bounded, available, tickSize],\n );\n\n // Rest is full opacity when there is no visible range to compare against. Compared against the\n // row's span rather than its marker, so a thinned rail still lights the tick the reader is under.\n const isActive = useCallback(\n (row: Row) => (visibleRange ? intersects(row.span, visibleRange) : true),\n [visibleRange],\n );\n\n // Not \"has focus\" — \"is being driven by the keyboard\". A click focuses a tick too, and gating on\n // focus left the card up after the pointer had gone: the reader clicked, moved away, and the\n // keyboard position they never asked for kept it open.\n const [keyboard, setKeyboard] = useState(false);\n\n // Whichever the reader is actually using. The pointer is the more direct statement, so it wins by\n // default — but not once the arrows are in play: clicking a tick and then pressing an arrow leaves\n // the pointer resting where it was clicked, and a pointer that always wins pins the card to a tick\n // the reader has already navigated away from. Pointing again takes it back (`onPointerEnter`).\n const shown = keyboard ? (navigated ?? pointer) : (pointer ?? navigated);\n\n // A backstop while a card is up: the pointer arriving anywhere outside the rail clears it.\n //\n // The tick's own leave is enough for a plain hover, and is not enough after a click — the popover\n // that opens brings focus guards that sit over the page, and the tick never sees the pointer go.\n // Verified with a real pointer: hovering away cleared it, clicking and then hovering away did not.\n // Asking the document is the one question no overlay can intercept.\n useEffect(() => {\n if (pointer == null) {\n return;\n }\n\n const rail = containerRef.current;\n const owner = rail?.ownerDocument;\n if (!rail || !owner) {\n return;\n }\n\n const onOver = (event: PointerEvent) => {\n if (!rail.contains(event.target as Node | null)) {\n setPointer(null);\n }\n };\n\n owner.addEventListener('pointerover', onOver);\n return () => owner.removeEventListener('pointerover', onOver);\n }, [pointer]);\n\n // Offset of the shown tick, read from the tick itself: the rail centres its ticks, so the position\n // is not `index * tickSize` and asking the element is the only answer that stays right.\n const [anchorOffset, setAnchorOffset] = useState(0);\n useLayoutEffect(() => {\n const row = shown == null ? null : rowsRef.current[shown];\n if (row) {\n setAnchorOffset(row.offsetTop + row.offsetHeight / 2);\n }\n }, [shown]);\n\n // Wave falloff: a Gaussian (normal-distribution) bell centred on the tick shown — a rounded\n // peak with inflected shoulders that flatten toward rest, rather than a dome. `WAVE_SPREAD` is\n // the standard deviation (in rows).\n const widthFor = useCallback(\n (index: number) => {\n if (shown == null) {\n return REST_WIDTH;\n }\n\n const distance = index - shown;\n const falloff = Math.exp(-(distance * distance) / (2 * WAVE_SPREAD * WAVE_SPREAD));\n return REST_WIDTH + (width - REST_WIDTH) * falloff;\n },\n [shown, width],\n );\n\n // Arrow keys walk the rail. The ticks are buttons, so tab reaches the rail and the arrows then\n // move within it — the roving behaviour a vertical list of controls is expected to have. The step\n // while the keyboard is still on a tick; and clamped to `rows`, since thinning can leave the ref\n // array longer than the rail it now renders.\n const handleKeyDown = useCallback(\n (event: ReactKeyboardEvent<HTMLDivElement>) => {\n const delta = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;\n if (!delta) {\n return;\n }\n\n // Navigation wins where the host offers it: a focused tick is a place in the document, so the\n // arrows move through the document rather than through the rail's own ticks — which are\n // thinned, and so are not one-to-one with anything the reader is counting.\n if (onNavigate) {\n event.preventDefault();\n setKeyboard(true);\n onNavigate(delta);\n return;\n }\n\n // Otherwise they walk the rail, the roving behaviour a vertical list of controls is expected\n // to have. Taken from the focused element rather than from the shown tick, and clamped to\n // `rows`, since thinning can leave the ref array longer than the rail it now renders.\n const current = rowsRef.current.findIndex((element) => element === event.target);\n if (current < 0) {\n return;\n }\n\n const next = Math.min(Math.max(current + delta, 0), rows.length - 1);\n event.preventDefault();\n rowsRef.current[next]?.focus();\n },\n [rows.length, onNavigate],\n );\n\n // While the rail has focus, the shown tick follows where the reader has got to.\n //\n // Navigation steps items and the rail shows ticks, and the two are not one-to-one — so after an\n // arrow press the popover would otherwise describe the tick the pointer last touched rather than\n // the place the reader is now. Derived from the visible range, so it tracks the list however the\n // reader moved: the arrows, the toolbar, or the scrollbar.\n const visibleFrom = visibleRange?.from;\n useEffect(() => {\n if (!keyboard || visibleFrom == null) {\n setNavigated(null);\n return;\n }\n\n const index = rows.findIndex((row) => visibleFrom < row.span.to);\n setNavigated(index < 0 ? rows.length - 1 : index);\n }, [keyboard, visibleFrom, rows]);\n\n const hoveredMarker = shown == null ? undefined : rows[shown]?.marker;\n\n return (\n <Popover.Root\n open={hoveredMarker != null}\n // Pinned to the anchor point rather than flipped into view, so it tracks the tick.\n positioning={{ placement: 'right', flip: false, slide: false }}\n autoFocus={false}\n >\n <div\n role='navigation'\n className={mx('relative flex flex-col justify-center overflow-hidden', classNames)}\n style={{\n width: `${width}px`,\n height: bounded ? '100%' : `${natural}px`,\n }}\n // Published so a test can ask *why* a card is up — the pointer being over a tick, or the\n // keyboard having navigated to one — rather than only that it is (§12).\n data-pointer={pointer ?? ''}\n data-navigated={navigated ?? ''}\n // And which of the two is actually being shown, since that is the invariant — the reader\n // sees one card, not two states (§12).\n data-shown={shown ?? ''}\n // A pointer anywhere in the rail ends the keyboard's claim on what is shown: the reader has\n // gone back to pointing, and the two should not both assert a position.\n onPointerDown={() => setKeyboard(false)}\n onBlurCapture={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n setKeyboard(false);\n }\n }}\n onKeyDown={handleKeyDown}\n ref={containerRef}\n >\n {/* Ticks are a fixed height, so a document with fewer markers than the rail can hold does not\n fill it — and they are centred rather than stacked at the top, which reads as the rail\n having been cut short. Centring is the rail's own business; where the *rail* sits is its\n container's. */}\n {rows.map((row, index) => {\n const marker = row.marker;\n const active = isActive(row);\n const tick = (\n <button\n key={marker.id}\n type='button'\n aria-label={marker.title}\n // Full-width row; flexes to fill a bounded rail height (min height keeps it hoverable\n // when the rail is unbounded), so any point in the rail maps to a tick. The focus\n // ring is suppressed because focus already shows as the tick extending and\n // darkening — an outline around an invisible full-width row reads as a stray box.\n // `flex-1` with `min-h-0`: the rows share whatever height the rail has, so N markers\n // spread over the full range rather than stacking at their natural pitch and leaving\n // the rest blank. Thinning above guarantees they never need less than the pitch.\n className='flex items-center w-full shrink-0 cursor-pointer outline-none'\n style={{ height: tickSize }}\n ref={(element) => {\n rowsRef.current[index] = element;\n }}\n onPointerEnter={() => {\n setPointer(index);\n // The reader has gone back to pointing, so the keyboard's claim on what is shown\n // ends here rather than at a blur that a mouse never causes.\n setKeyboard(false);\n }}\n // Cleared on the tick that set it, rather than on the rail. A rail-level leave is\n // synthesised from over/out pairs and was observed not firing at all — the card\n // stayed up after the pointer had gone, in the browser and in a test. Moving between\n // ticks does not flicker: the leave and the next enter land in one batch.\n onPointerLeave={() => setPointer((current) => (current === index ? null : current))}\n onFocus={() => keyboard && setNavigated(index)}\n onClick={() => onSelect?.(marker, row.index)}\n >\n <div\n className={mx(\n 'h-[3px] rounded-full transition-all duration-200 ease-out',\n shown === index ? 'bg-neutral-800 dark:bg-neutral-200' : 'bg-neutral-400 dark:bg-neutral-600',\n active ? 'opacity-100' : 'opacity-30',\n )}\n style={{ width: widthFor(index) }}\n />\n </button>\n );\n\n return tick;\n })}\n\n {/* A separate anchor, and the ticks are never wrapped.\n Wrapping the shown tick in `Popover.Anchor` changes the element type at that position, so\n React unmounts and remounts that button — and a destroyed element never receives\n `pointerleave`, which is why the card would not dismiss when the pointer left. Verified\n with a real pointer: `data-pointer` stayed on the tick after the pointer had gone.\n A zero-height anchor moved to the shown tick's offset keeps every tick stable; the\n popover is keyed to the marker so it still re-measures when the anchor moves. */}\n <Popover.Anchor asChild>\n <div className='absolute left-0' style={{ top: anchorOffset, width, height: 0 }} />\n </Popover.Anchor>\n </div>\n {hoveredMarker && (\n <Popover.Content\n // Keyed to the tick: the popover measures its anchor when it mounts, and moving the anchor\n // to a different element does not make it measure again — the card stayed put while the\n // pointer walked the rail, drifting further from the tick with every step. Remounting per\n // tick is what makes it re-measure.\n key={hoveredMarker.id}\n // Wide enough for a prompt's first lines, but never past the space beside the rail.\n classNames='w-[32rem] max-w-(--available-width)'\n >\n {/* A plain column rather than `Popover.Body`: a hover card neither scrolls nor needs the body's gutter grid. */}\n <Layout.Flex column classNames='gap-1 px-2 py-1'>\n <p className='font-medium line-clamp-2'>{hoveredMarker.title}</p>\n {hoveredMarker.description && (\n <p className='text-sm text-fg-muted line-clamp-4'>{hoveredMarker.description}</p>\n )}\n </Layout.Flex>\n </Popover.Content>\n )}\n </Popover.Root>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"chunk-src.mjs","names":["_Fragment","_Fragment","DEFAULT_WIDTH","_Fragment","_jsx"],"sources":["../../src/hooks/useDecorations.tsx","../../src/hooks/useFeedNavigation.ts","../../src/hooks/useItemSelection.tsx","../../src/components/Block/selection-group.ts","../../src/components/Block/highlight.ts","../../src/components/Block/widget-state.tsx","../../src/components/Block/HtmlBlock.tsx","../../src/components/Block/diff-range.ts","../../src/components/Block/extensions.ts","../../src/components/Block/MarkdownBlock.tsx","../../src/model/feed-model.ts","../../src/model/feed.ts","../../src/model/useListModel.ts","../../src/components/MessageList/position-log.ts","../../src/components/MessageList/MessageList.tsx","../../src/components/Minimap/Minimap.tsx","../../src/components/Outline/Outline.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { type PropsWithChildren, createContext, useContext, useMemo } from 'react';\n\n/**\n * A range of one item's rendered text that some producer wants painted (SPEC: Decoration).\n *\n * Search is ONE producer — mentions, lint results and diff spans are others — which is why this is\n * not called a hit: the item painting the range does not care where it came from, and the list does\n * not know it exists at all.\n */\nexport type Decoration = {\n /** Item (message) id. */\n id: string;\n range: { offset: number; length: number };\n /** Producer tag: 'search' | 'mention' | ... */\n kind: string;\n};\n\nconst DecorationsContext = createContext<ReadonlyMap<string, readonly Decoration[]>>(new Map());\n\nexport type DecorationsProviderProps = PropsWithChildren<{\n decorations?: readonly Decoration[];\n}>;\n\n/**\n * THE PATTERN for cross-cutting per-item data (SPEC §Aspects), not a feed feature: a provider at\n * the host carries what producers made, and an item asks for its own by id. The list neither knows\n * nor routes — `hits` left its API entirely. Grouped once per change rather than filtered per item,\n * so a long feed stays O(decorations).\n */\nexport const DecorationsProvider = ({ decorations, children }: DecorationsProviderProps) => {\n const byId = useMemo(() => {\n const map = new Map<string, Decoration[]>();\n for (const decoration of decorations ?? []) {\n const list = map.get(decoration.id) ?? [];\n list.push(decoration);\n map.set(decoration.id, list);\n }\n\n return map;\n }, [decorations]);\n\n return <DecorationsContext.Provider value={byId}>{children}</DecorationsContext.Provider>;\n};\n\nconst NONE: readonly Decoration[] = [];\n\n/** An item's own decorations. Items work without a provider — they are simply undecorated. */\nexport const useDecorations = (id: string): readonly Decoration[] => {\n return useContext(DecorationsContext).get(id) ?? NONE;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type RefObject, useCallback, useMemo, useRef } from 'react';\n\nimport { type WindowController } from '@dxos/react-ui-virtual';\n\nimport { type Stop } from '../model/index.ts';\n\n/** How long a commanded destination outranks the scroll offset as \"where the reader is\". */\nconst PENDING_WINDOW = 1_500;\n\nexport type UseFeedNavigationOptions = {\n controller: RefObject<WindowController | null>;\n /** The stops policy's current answer; asked per press, so a policy change needs no rewiring. */\n stops: () => readonly Stop[];\n /** The row containing the scroll offset — the cursor, derived and never set beside the scroll. */\n current: () => number;\n count: () => number;\n /**\n * Told before every jump, with the target index — the follow's `onNavigate` goes here. Without\n * it a navigation races the follow's correction effect, which runs before the jump's scroll\n * event can withdraw the intent and snaps the reader straight back to the tail they just left.\n * The follow may move the view only while content is arriving AND the reader has pinned\n * themselves to the bottom; navigating into the feed un-pins by definition.\n */\n onNavigate?: (index: number) => void;\n};\n\nexport type FeedNavigation = {\n /** Move by `delta` stops. Every press travels: see below. */\n step: (delta: number) => void;\n /** Jump to a stop (or a raw index), aligning the last row to the end where that is what it means. */\n jumpTo: (index: number, behavior?: ScrollBehavior) => void;\n first: () => void;\n last: () => void;\n};\n\n/**\n * The one navigation seam (SPEC: useFeedNavigation): toolbar buttons, arrow keys, the outline rail\n * and the minimap all call this, so they cannot disagree about what a step is.\n *\n * Stepping is from the row containing the scroll offset. The stop above it begins strictly higher\n * and the stop below strictly lower, so every press moves the viewport — an index compared against\n * a position cannot promise that while positions are still being measured. Steps glide (corrections\n * move the window, not the scroll, so nothing cancels the animation); far jumps are instant, since\n * a glide across unmounted rows is a journey over a blank screen.\n *\n * Distinct from react-ui-list's `useListNavigation`, which is focus traversal over fully-mounted\n * rows — a different problem in the same naming family.\n */\nexport const useFeedNavigation = ({\n controller,\n stops,\n current,\n count,\n onNavigate,\n}: UseFeedNavigationOptions): FeedNavigation => {\n // The last commanded destination, while its travel may still be in flight. A step during a glide\n // must chain from where the reader is *going*, not from wherever the scroll happens to be this\n // frame — deriving from the offset mid-travel re-finds the same stop, and rapid presses swallow\n // each other (seen live: the toolbar's up button \"not reliably\" stepping). Expires rather than\n // waiting on arrival, because a scroll the reader takes over is theirs again.\n const pending = useRef<{ index: number; at: number } | undefined>(undefined);\n\n // Hosts hand the accessors as inline closures, so their identity churns per render; read through\n // a ref so the seam itself is stable — a controller derived from it and stored in a host's state\n // otherwise republishes every render, which is a setState-in-effect loop.\n const optionsRef = useRef({ stops, current, count, onNavigate });\n optionsRef.current = { stops, current, count, onNavigate };\n\n const jumpTo = useCallback(\n (index: number, behavior: ScrollBehavior = 'auto') => {\n const { count, onNavigate } = optionsRef.current;\n pending.current = { index, at: performance.now() };\n onNavigate?.(index);\n const align = index >= count() - 1 ? 'end' : 'start';\n controller.current?.scrollToIndex(index, align, behavior);\n },\n [controller],\n );\n\n const step = useCallback(\n (delta: number) => {\n const { stops, current } = optionsRef.current;\n const all = stops();\n if (!all.length) {\n return;\n }\n\n const flight = pending.current;\n const at =\n flight && performance.now() - flight.at < PENDING_WINDOW && flight.index !== current()\n ? flight.index\n : current();\n const next =\n delta > 0\n ? (all.find(({ index }) => index > at) ?? all[all.length - 1])\n : ([...all].reverse().find(({ index }) => index < at) ?? all[0]);\n\n jumpTo(next.index, 'smooth');\n },\n [jumpTo],\n );\n\n const first = useCallback(() => jumpTo(0), [jumpTo]);\n\n const last = useCallback(() => {\n const { count } = optionsRef.current;\n jumpTo(count() - 1);\n }, [jumpTo]);\n\n return useMemo(() => ({ step, jumpTo, first, last }), [step, jumpTo, first, last]);\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { type MouseEvent, type PropsWithChildren, createContext, useCallback, useContext } from 'react';\n\n/** What an item can do about its own selection — the item-side face of the host's selection model. */\nexport type ItemSelection = {\n selected: boolean;\n /** Additive when the gesture says so (meta/ctrl); the mode is the provider's business. */\n toggle: (additive?: boolean) => void;\n};\n\nexport type ItemSelectionValue = {\n selectedIds?: ReadonlySet<string>;\n onSelect?: (id: string, additive: boolean) => void;\n};\n\nconst ItemSelectionContext = createContext<ItemSelectionValue>({});\n\nexport type ItemSelectionProviderProps = PropsWithChildren<ItemSelectionValue>;\n\n/**\n * The item-side hook of the decoration pattern applied to selection (SPEC: useItemSelection): an\n * item asks for its own `{ selected, toggle }` by id instead of the list threading `selectedIds`\n * through props. Selection is model-space ids, so virtualization is untouched: an unmounted item is\n * still selected, and mounts knowing it.\n *\n * The provider is deliberately the thinnest thing that works — a set and a callback. Mode policies\n * (single | multi | range) belong to `react-ui-list`'s `useListSelection`, which a host composes\n * *above* this provider; teaching this package about modes would be rebuilding that hook.\n */\nexport const ItemSelectionProvider = ({ selectedIds, onSelect, children }: ItemSelectionProviderProps) => (\n <ItemSelectionContext.Provider value={{ selectedIds, onSelect }}>{children}</ItemSelectionContext.Provider>\n);\n\n/**\n * The whole selection value, for a component that renders many items in one pass.\n *\n * A row loop cannot call `useItemSelection` per row — hooks do not run in loops — so the viewport\n * reads the value once and derives per-row state; item-shaped components use the per-id hook.\n */\nexport const useItemSelectionValue = (): ItemSelectionValue => useContext(ItemSelectionContext);\n\nexport const useItemSelection = (id: string): ItemSelection => {\n const { selectedIds, onSelect } = useContext(ItemSelectionContext);\n const toggle = useCallback((additive = true) => onSelect?.(id, additive), [id, onSelect]);\n return { selected: selectedIds?.has(id) ?? false, toggle };\n};\n\n/** Reads the additive intent off a gesture, so callers do not each invent the modifier rule. */\nexport const isAdditive = (event: MouseEvent | globalThis.MouseEvent): boolean =>\n event.metaKey || event.ctrlKey || event.shiftKey;\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type EditorView } from '@codemirror/view';\nimport { createContext, useContext, useMemo } from 'react';\n\n/**\n * Coordinates selection across the items of one feed.\n *\n * Each item is its own editor with its own selection state, and `drawSelection` paints that state\n * whether or not the view has focus — so without a coordinator every message the reader has ever\n * selected in stays highlighted at once. The feed has one selection, so claiming it in one item\n * collapses it everywhere else.\n */\nexport type SelectionGroup = {\n /** Adds a view to the group; returns the disposer. */\n register: (view: EditorView) => () => void;\n /** Makes `view` the sole owner of the feed's selection. */\n claim: (view: EditorView) => void;\n};\n\n/** Creates a group; one per feed, provided to its items by `MessageList.Root`. */\nexport const createSelectionGroup = (): SelectionGroup => {\n const views = new Set<EditorView>();\n\n return {\n register: (view) => {\n views.add(view);\n return () => views.delete(view);\n },\n\n claim: (view) => {\n for (const other of views) {\n if (other === view || other.state.selection.main.empty) {\n continue;\n }\n // Collapsing re-enters this listener for `other`, but with an empty selection, which claims\n // nothing — so the cascade terminates after one pass.\n other.dispatch({ selection: { anchor: 0 } });\n }\n },\n };\n};\n\nexport const SelectionGroupContext = createContext<SelectionGroup | undefined>(undefined);\n\n/** The enclosing feed's selection group, or a private one when an item is used standalone. */\nexport const useSelectionGroup = (): SelectionGroup => {\n const provided = useContext(SelectionGroupContext);\n const fallback = useMemo(createSelectionGroup, []);\n return provided ?? fallback;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { StateEffect, StateField } from '@codemirror/state';\nimport { Decoration, type DecorationSet, EditorView } from '@codemirror/view';\n\n/** Ranges to highlight, as [from, to) offsets into the item's document. */\nexport type HighlightRange = readonly [number, number];\n\nexport const setHighlights = StateEffect.define<readonly HighlightRange[]>();\n\nconst mark = Decoration.mark({ class: 'dx-feed-hit' });\n\nconst clamp = (offset: number, length: number) => Math.max(0, Math.min(offset, length));\n\n/**\n * Highlights pushed in from the model.\n *\n * Search runs over the feed's projection rather than per-view (most messages are never mounted), so\n * the item receives ranges instead of owning search state.\n */\nexport const highlights = StateField.define<DecorationSet>({\n create: () => Decoration.none,\n update: (value, tr) => {\n for (const effect of tr.effects) {\n if (effect.is(setHighlights)) {\n const length = tr.state.doc.length;\n return Decoration.set(\n effect.value\n // Clamp against the current document at both ends: a hit computed on the model can\n // outrun a view whose streaming tail has not arrived yet, and a decoration outside\n // `[0, length]` throws rather than being ignored.\n .map(([from, to]) => [clamp(from, length), clamp(to, length)] as const)\n .filter(([from, to]) => from < to)\n .map(([from, to]) => mark.range(from, to)),\n true,\n );\n }\n }\n\n return tr.docChanged ? value.map(tr.changes) : value;\n },\n provide: (field) => EditorView.decorations.from(field),\n});\n\nexport const highlightTheme = EditorView.baseTheme({\n '.dx-feed-hit': {\n backgroundColor: 'var(--dx-accentFill)',\n color: 'var(--dx-accentFillText)',\n borderRadius: '2px',\n },\n});\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { type PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from 'react';\n\n/**\n * State a widget keeps that must outlive the row it is drawn in.\n *\n * A virtualized feed destroys a row when it leaves the window, and React state inside a widget dies\n * with it — so a reader who expands a tool panel, scrolls away and comes back finds it shut. In a\n * thread that is one long document the question never arises, because nothing ever unmounts; it is\n * the cost of virtualization, and it has to be paid somewhere above the row.\n *\n * Here, in a map owned by `MessageList.Root`. Not in the message, because whether a panel is open is\n * the reader's business and not part of what was said.\n */\nexport type WidgetStateStore = {\n read: (key: string) => unknown;\n write: (key: string, value: unknown) => void;\n};\n\nconst WidgetStateContext = createContext<WidgetStateStore | undefined>(undefined);\n\n/** Scopes a widget's key to its message, so two messages' widgets cannot collide. */\nconst WidgetScopeContext = createContext<string>('');\n\nexport const WidgetStateProvider = ({ store, children }: PropsWithChildren<{ store: WidgetStateStore }>) => (\n <WidgetStateContext.Provider value={store}>{children}</WidgetStateContext.Provider>\n);\n\nexport const WidgetScopeProvider = ({ scope, children }: PropsWithChildren<{ scope: string }>) => (\n <WidgetScopeContext.Provider value={scope}>{children}</WidgetScopeContext.Provider>\n);\n\n/** Creates the store a feed's widgets share. One per `MessageList.Root`. */\nexport const createWidgetStateStore = (): WidgetStateStore => {\n const values = new Map<string, unknown>();\n return {\n read: (key) => values.get(key),\n write: (key, value) => {\n values.set(key, value);\n },\n };\n};\n\n/**\n * `useState` for a widget, keyed so it survives the row being destroyed and rebuilt.\n *\n * The key must identify the widget within its message — its tag and position in the document, say —\n * and is scoped to the message automatically. Outside a feed this degrades to plain `useState`, so a\n * widget can still be rendered on its own.\n */\nexport const useWidgetState = <T,>(key: string, initial: T): [T, (value: T) => void] => {\n const store = useContext(WidgetStateContext);\n const scope = useContext(WidgetScopeContext);\n const scoped = useMemo(() => `${scope}:${key}`, [scope, key]);\n\n // Seeded from the store on mount, which is the whole point: a remounted widget starts from what\n // the reader last left it at rather than from the default.\n const [value, setValue] = useState<T>(() => {\n const stored = store?.read(scoped);\n return (stored === undefined ? initial : stored) as T;\n });\n\n const set = useCallback(\n (next: T) => {\n store?.write(scoped, next);\n setValue(next);\n },\n [store, scoped],\n );\n\n return [value, set];\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport DOMPurify from 'dompurify';\nimport React, { memo, useMemo } from 'react';\n\nexport type HtmlBlockProps = {\n html: string;\n};\n\n/**\n * A message rendered as sanitized HTML — the email case, which is not markdown and cannot be a\n * CodeMirror document.\n *\n * Its presence is the reason the item renderer is generalized rather than markdown-only: the\n * engine owns virtualization, chrome and selection anchors, not what a message looks like inside.\n * As plain (non-contenteditable) DOM it is also the control case for cross-item selection.\n */\nexport const HtmlBlock = memo(({ html }: HtmlBlockProps) => {\n const sanitized = useMemo(() => DOMPurify.sanitize(html, { USE_PROFILES: { html: true } }), [html]);\n\n return <div className='dx-feed-html' dangerouslySetInnerHTML={{ __html: sanitized }} />;\n});\n\nHtmlBlock.displayName = 'HtmlBlock';\n","//\n// Copyright 2026 DXOS.org\n//\n\n/** One replacement taking `from` to `to`, as a CodeMirror change spec. */\nexport type RangeChange = { from: number; to: number; insert: string };\n\n/**\n * The smallest single replacement that turns `before` into `after`: the common prefix and suffix\n * are left alone, so a document whose middle changed keeps the decorations either side of it.\n */\nexport const diffRange = (before: string, after: string): RangeChange => {\n const limit = Math.min(before.length, after.length);\n let prefix = 0;\n while (prefix < limit && before.charCodeAt(prefix) === after.charCodeAt(prefix)) {\n prefix++;\n }\n\n // Bounded by the prefix, so a repeated run is never counted on both sides.\n let suffix = 0;\n while (\n suffix < limit - prefix &&\n before.charCodeAt(before.length - 1 - suffix) === after.charCodeAt(after.length - 1 - suffix)\n ) {\n suffix++;\n }\n\n return { from: prefix, to: before.length - suffix, insert: after.slice(prefix, after.length - suffix) };\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Extension } from '@codemirror/state';\nimport { EditorView } from '@codemirror/view';\n\nimport {\n type ObjectLinkProps,\n type WidgetDef,\n WidgetHostOptions,\n type XmlWidgetRegistry,\n createBasicExtensions,\n createMarkdownExtensions,\n createThemeExtensions,\n decorateMarkdown,\n extendedMarkdown,\n objectLinks,\n widgetHost,\n xmlBlockDecoration,\n xmlFormatting,\n xmlTags,\n} from '@dxos/ui-editor';\n\nimport { highlights, highlightTheme } from './highlight.ts';\n\nexport type ItemExtensionOptions = {\n registry?: XmlWidgetRegistry;\n /** The block widget for `![label](echo://…)` — an object embedded as a card; none by default. */\n objectImage?: WidgetDef<ObjectLinkProps>;\n editable?: boolean;\n themeMode?: 'light' | 'dark';\n setWidgets?: WidgetHostOptions['setWidgets'];\n};\n\n/**\n * The extension set one item's document is built from.\n *\n * Everything that does not depend on the item is **built once and shared**, and that is the point of\n * this module rather than a tidiness. `EditorView.theme()` mints a new `StyleModule` and a new\n * generated class on every call, so a set built per item injects one stylesheet per row and\n * invalidates the whole document's style each time it does. Measured at ~14ms per row against ~2ms\n * for the identical set shared (`baseline/mount`) — six times the cost of the first fill, for\n * stylesheets that are all the same.\n *\n * Extensions are descriptors, not instances: a `StateField` or `ViewPlugin` in a shared array is\n * still instantiated per view, so sharing changes nothing but the identity CodeMirror dedupes on.\n */\nexport const createBlockExtensions = ({\n registry,\n objectImage,\n editable = false,\n themeMode = 'light',\n setWidgets,\n}: ItemExtensionOptions = {}): Extension[] => [\n ...sharedExtensions(registry, editable, themeMode),\n // The one part that cannot be shared: the callback that hands this item's widgets back to it.\n ...(registry\n ? [widgetHost({ setWidgets, bookmarks: ['prompt'] }), xmlTags({ registry }), objectLinks({ image: objectImage })]\n : []),\n];\n\n/** Registries are compared by identity, so a feed's single registry is a single cache scope. */\nconst NO_REGISTRY = {};\n\nconst cache = new WeakMap<object, Map<string, Extension[]>>();\n\nconst sharedExtensions = (\n registry: XmlWidgetRegistry | undefined,\n editable: boolean,\n themeMode: 'light' | 'dark',\n): Extension[] => {\n const scope = registry ?? NO_REGISTRY;\n let byVariant = cache.get(scope);\n if (!byVariant) {\n byVariant = new Map();\n cache.set(scope, byVariant);\n }\n\n const key = `${editable}:${themeMode}`;\n let extensions = byVariant.get(key);\n if (!extensions) {\n extensions = build(registry, editable, themeMode);\n byVariant.set(key, extensions);\n }\n\n return extensions;\n};\n\nconst build = (registry: XmlWidgetRegistry | undefined, editable: boolean, themeMode: 'light' | 'dark'): Extension[] =>\n [\n createBasicExtensions({ readOnly: !editable, editable, lineWrapping: true }),\n // Colours fenced code, as the markdown editor and the previous chat stream do.\n createThemeExtensions({ themeMode, syntaxHighlighting: true }),\n // A registry changes how the document is *parsed*, not only how it is decorated: registered\n // tags have to survive as single blocks through the markdown parser before `xmlTags` can\n // replace them, and without that they render as the literal angle brackets they are.\n registry ? extendedMarkdown({ registry }) : createMarkdownExtensions(),\n // `prompt` is skipped only when its block decoration frames it; otherwise its tags are text like any other.\n registry && xmlFormatting({ skip: registry.prompt ? ['prompt'] : [] }),\n decorateMarkdown(),\n // The tags are hidden but the prompt is NOT framed here: the frame is chrome's, which also\n // owns the rewind toolbar under it. Styling it in both places drew the border twice.\n registry?.prompt && xmlBlockDecoration({ tag: 'prompt', hideTags: true }),\n highlights,\n highlightTheme,\n blockScrollerTheme,\n ].filter(Boolean) as Extension[];\n\n/**\n * An item's editor never scrolls on the block axis: it is auto-height and the feed around it is\n * what scrolls, so its content fits by construction.\n *\n * Left to `auto`, that invariant is broken transiently by any widget that changes height — a tool\n * block's disclosure animating open grows the content every frame while `requestMeasure` lands the\n * editor's new height a frame later, so the scroller is a frame short throughout and paints a\n * scrollbar for the length of the animation. The inline axis keeps `auto`; a wide payload still\n * scrolls.\n */\nconst blockScrollerTheme = EditorView.theme({\n '.cm-scroller': {\n overflowY: 'hidden',\n },\n});\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { EditorState, type Extension } from '@codemirror/state';\nimport { EditorView } from '@codemirror/view';\nimport React, { memo, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport type * as Util from '@dxos/react-ui/Util';\nimport { type ObjectLinkProps, type WidgetDef, type WidgetState, type XmlWidgetRegistry } from '@dxos/ui-editor';\nimport { mx } from '@dxos/ui-theme';\n\nimport { diffRange } from './diff-range.ts';\nimport { createBlockExtensions } from './extensions.ts';\nimport { type HighlightRange, setHighlights } from './highlight.ts';\nimport { useSelectionGroup } from './selection-group.ts';\n\nexport type MarkdownBlockProps = Util.ThemedClassName<{\n text: string;\n /**\n * Drip appended text in per frame (the typewriter) instead of dispatching whole deltas. The\n * source's chunk size is whatever the transport delivered — often a whole block — and painting\n * that in one frame reads as blocks appearing fully formed rather than an answer being written.\n * When it turns off (the turn ended), whatever is still queued lands at once.\n */\n stream?: boolean;\n /**\n * When false the view mounts with `EditorView.editable.of(false)`, which drops `contenteditable`\n * from the DOM. Browsers refuse to extend one selection across two contenteditable hosts, so this\n * is what decides whether a drag can span adjacent items.\n */\n editable?: boolean;\n registry?: XmlWidgetRegistry;\n /** The block widget for an object embedded as a card (`![label](echo://…)`). */\n objectImage?: WidgetDef<ObjectLinkProps>;\n hits?: readonly HighlightRange[];\n /** Number of block widgets this item currently has mounted; 0 once it unmounts. */\n onWidgetsChange?: (count: number) => void;\n}>;\n\n/**\n * One message as its own markdown document.\n *\n * Each item owns a document, so streaming appends and per-message editing stay local — where a\n * single thread-wide document needs a cursor and a range table to know which message it is touching.\n */\nexport const MarkdownBlock = memo(\n ({\n classNames,\n text,\n stream,\n editable = false,\n registry,\n objectImage,\n hits,\n onWidgetsChange,\n }: MarkdownBlockProps) => {\n const themeMode = Hooks.useThemeMode();\n const [view, setView] = useState<EditorView | null>(null);\n // React widgets render in portals into hosts the extension places in the document, so the item has\n // to own them: a widget's tree belongs to the React root that rendered the item, not to CodeMirror.\n const [widgets, setWidgets] = useState<WidgetState[]>([]);\n const rootRef = useRef<HTMLDivElement>(null);\n\n // Read through a ref so the group never lands in the extension deps: rebuilding extensions\n // reconstructs the view, which would discard the selection the reader just made.\n const selectionGroup = useSelectionGroup();\n const selectionGroupRef = useRef(selectionGroup);\n selectionGroupRef.current = selectionGroup;\n\n const extensions = useMemo<Extension[]>(\n () => [\n ...createBlockExtensions({ registry, objectImage, editable, themeMode, setWidgets }),\n EditorView.updateListener.of((update) => {\n if (update.selectionSet && !update.state.selection.main.empty) {\n selectionGroupRef.current.claim(update.view);\n }\n }),\n ],\n [editable, themeMode, registry, objectImage],\n );\n\n // Deliberately NOT `useTextEditor`, which builds the view in a passive effect — i.e. after paint.\n // The virtualizer measures each row as it mounts, so a row whose document does not exist yet\n // measures at chrome height and then grows, shifting every row below it. Constructing the view in\n // the layout phase gives the row its real height before the first paint, so it is measured once.\n const initialTextRef = useRef(text);\n useLayoutEffect(() => {\n const parent = rootRef.current;\n if (!parent) {\n return;\n }\n\n // Built detached and attached once: given a `parent`, the view writes its attributes on a live\n // element, and each write invalidates the document's style for every row that mounts.\n const instance = new EditorView({\n state: EditorState.create({ doc: initialTextRef.current, extensions }),\n });\n parent.appendChild(instance.dom);\n const unregister = selectionGroupRef.current.register(instance);\n setView(instance);\n\n return () => {\n unregister();\n instance.destroy();\n setView(null);\n };\n }, [extensions]);\n\n // The document belongs to the model, not the view: a message whose text changes is reconciled\n // rather than remounted, so the virtualizer's measurement for this row survives the update.\n //\n // Two cursors: `targetRef` is the model's latest text, `dispatchedRef` is what the document\n // holds. Outside streaming they move together; while streaming, the drip below advances\n // `dispatchedRef` a few characters per frame toward the target.\n const targetRef = useRef(text);\n const dispatchedRef = useRef(text);\n const dripRaf = useRef(0);\n\n useEffect(() => {\n if (!view) {\n return;\n }\n\n targetRef.current = text;\n initialTextRef.current = text;\n if (dispatchedRef.current === text) {\n return;\n }\n\n // A non-append (a rewrite, a view-type switch, a status tag changing) lands atomically —\n // dripping a replacement would show the reader a half-rewritten document — and replaces only\n // the span that differs, so the decorations and widgets either side of it survive.\n if (!text.startsWith(dispatchedRef.current)) {\n cancelAnimationFrame(dripRaf.current);\n dripRaf.current = 0;\n dispatchedRef.current = text;\n view.dispatch({ changes: diffRange(view.state.doc.toString(), text) });\n return;\n }\n\n if (!stream) {\n // The turn is over (or was never streaming): whatever is pending lands at once.\n cancelAnimationFrame(dripRaf.current);\n dripRaf.current = 0;\n const from = dispatchedRef.current.length;\n dispatchedRef.current = text;\n view.dispatch({ changes: { from, insert: text.slice(from) } });\n return;\n }\n\n // The typewriter: a bounded slice of the backlog per frame, so the cadence is a write rather\n // than a paste and a burst still catches up within a second. XML tags land whole — a dripped\n // half-tag would flash literal markup before the parser can classify it.\n if (dripRaf.current) {\n return;\n }\n\n const tick = () => {\n const target = targetRef.current;\n const dispatched = dispatchedRef.current;\n if (!target.startsWith(dispatched)) {\n // The target was rewritten mid-drip; the effect above handles it on its next run.\n dripRaf.current = 0;\n return;\n }\n\n const backlog = target.slice(dispatched.length);\n if (!backlog.length) {\n dripRaf.current = 0;\n return;\n }\n\n let take = Math.max(2, Math.ceil(backlog.length / 30));\n if (backlog[0] === '<') {\n const close = backlog.indexOf('>');\n take = close === -1 ? 0 : Math.max(take, close + 1);\n } else {\n const tag = backlog.indexOf('<');\n if (tag !== -1) {\n take = Math.min(take, tag);\n }\n }\n\n if (take > 0) {\n const insert = backlog.slice(0, take);\n dispatchedRef.current = dispatched + insert;\n view.dispatch({ changes: { from: dispatched.length, insert } });\n }\n\n dripRaf.current = requestAnimationFrame(tick);\n };\n\n dripRaf.current = requestAnimationFrame(tick);\n }, [view, text, stream]);\n\n // The queue dies with the item; the document is rebuilt from the model on the next mount.\n useEffect(() => () => cancelAnimationFrame(dripRaf.current), []);\n\n // Nothing is dispatched to clear highlights that were never set: a transaction makes CodeMirror\n // re-measure the document, and a viewport of rows mounting is a viewport of forced layouts for a\n // search that is not running.\n const highlighted = useRef(false);\n useEffect(() => {\n if (!view || (!hits?.length && !highlighted.current)) {\n return;\n }\n\n highlighted.current = Boolean(hits?.length);\n view.dispatch({ effects: setHighlights.of(hits ?? []) });\n }, [view, hits]);\n\n // Reported rather than derived: the widgets are portals into the item's own document, so nothing\n // above the item can count them without reaching into the DOM.\n useEffect(() => {\n onWidgetsChange?.(widgets.length);\n return () => onWidgetsChange?.(0);\n }, [widgets.length, onWidgetsChange]);\n\n // A block widget is measured once, when CodeMirror mounts it. Its content is React rendered into a\n // portal, so anything that changes height afterwards — a disclosure opening, an image loading —\n // leaves the editor holding a stale height and drawing the lines below it over the widget.\n //\n // Observing the portal roots and asking for a re-measure is what keeps the document's geometry\n // honest; the row's own height then follows through the virtualizer's observer.\n useEffect(() => {\n if (!view || !widgets.length) {\n return;\n }\n\n const observer = new ResizeObserver(() => view.requestMeasure());\n for (const { root } of widgets) {\n observer.observe(root);\n }\n\n return () => observer.disconnect();\n }, [view, widgets]);\n\n return (\n <>\n {/* No query container here: a prompt's bubble is sized by this element's text, which\n inline-size containment would stop contributing. */}\n <div className={mx(classNames)} ref={rootRef} />\n {widgets.map(({ Component, root, id, props }) => (\n <div key={id}>{createPortal(<Component view={view ?? undefined} {...props} />, root)}</div>\n ))}\n </>\n );\n },\n);\n\nMarkdownBlock.displayName = 'MarkdownBlock';\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type ContentBlock, type Message } from '@dxos/types';\n\n/**\n * What an item renders. The engine is renderer-agnostic: a message may resolve to a markdown\n * document, to raw HTML (email bodies, which are not markdown and cannot be a CodeMirror document),\n * or to a caller-supplied component.\n */\nexport type ItemContent =\n | { kind: 'markdown'; text: string }\n | { kind: 'html'; html: string }\n | { kind: 'custom'; key: string; data?: unknown };\n\n/** Resolves a message to the content its item renders. One per scenario (chat, email, …). */\nexport type MessageRenderer = (message: Message.Message) => ItemContent;\n\n/**\n * Default renderer: an `text/html` block wins (email), otherwise every block is flattened to\n * markdown. Block kinds the engine does not know become fenced JSON so nothing renders as blank.\n */\nexport const defaultRenderer: MessageRenderer = (message) => createContent(message, false);\n\n/**\n * Assistant chat: as {@link defaultRenderer}, plus the reader's own turns marked with `<prompt>`.\n *\n * Separate because the tag is only meaningful where a widget registry knows it — a feed rendered\n * without one would show the reader their own angle brackets, which is what a human chat, a\n * transcript and a comment list all are.\n */\nexport const chatRenderer: MessageRenderer = (message) => createContent(message, true);\n\nconst createContent = (message: Message.Message, prompts: boolean): ItemContent => {\n const textBlocks = message.blocks.filter((block): block is ContentBlock.Text => block._tag === 'text');\n const html = textBlocks.find((block) => block.mimeType === 'text/html');\n if (html) {\n return { kind: 'html', html: html.text };\n }\n\n return {\n kind: 'markdown',\n text: message.blocks\n .map((block) => blockToMarkdown(message, block, prompts))\n .filter(Boolean)\n .join('\\n\\n'),\n };\n};\n\n/**\n * One block as markdown, with anything that is not prose emitted as an XML tag for a widget to\n * render. A message is the concatenation of these, which is what makes an item a document rather\n * than a component tree: the blocks are content, and the chrome around them belongs to the host.\n */\nconst blockToMarkdown = (message: Message.Message, block: ContentBlock.Any, prompts: boolean): string => {\n switch (block._tag) {\n case 'text':\n // A prompt is marked rather than styled: the reader's own words are content, and the widget\n // that frames them is the host's business.\n return prompts && message.sender.role === 'user' ? tag('prompt', block.text, block) : block.text.trim();\n case 'reasoning':\n return tag('reasoning', block.reasoningText ?? block.redactedText ?? '', block);\n case 'status':\n return tag('status', block.statusText, block);\n case 'summary':\n return tag('summary', block.content, block);\n case 'suggestion':\n return tag('suggestion', block.text, block);\n case 'select':\n return block.options.length\n ? `<select>${block.options.map((option) => `<option>${escapeXml(option)}</option>`).join('')}</select>`\n : '';\n case 'toolCall':\n // Name and status ride on the tag rather than in widget state kept beside the document: an\n // item is rebuilt from its message alone, so anything the widget needs has to survive there.\n return `<toolCall id=\"${escapeXml(block.toolCallId)}\" name=\"${escapeXml(block.name)}\"${block.pending ? ' pending=\"true\"' : ''} />`;\n case 'toolResult':\n return tag('toolResult', String(block.result ?? block.error ?? ''), block, `for=\"${escapeXml(block.name)}\"`);\n default:\n // Nothing renders blank: an unknown block is shown as what it is.\n return tag('json', JSON.stringify(block), block);\n }\n};\n\n/**\n * An XML tag holding block content.\n *\n * A pending block is emitted **unclosed**, which is the streaming contract: the item reconciles by\n * appending, so a closing tag written before the content is complete would have to be rewritten on\n * every chunk — and rewriting the document is what discards decorations and scroll position.\n *\n * Paragraph breaks inside the content are collapsed, since a blank line would end the enclosing\n * markdown block and leave the rest of the tag parsing as prose.\n */\nconst tag = (name: string, content: string, block: ContentBlock.Any, attributes?: string): string => {\n const text = escapeXml(content.replace(/\\n\\n+/g, ' ').trim());\n if (!text.length && !block.pending) {\n return '';\n }\n\n const open = `<${name}${attributes ? ` ${attributes}` : ''}>`;\n return block.pending ? `${open}${text}` : `${open}${text}</${name}>`;\n};\n\nconst escapeXml = (raw: string): string => raw.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n\n/**\n * Whether a message is the reader's own prompt. Role alone cannot say: tool results travel back as\n * `user`-role messages (the AI protocol convention), and a synthetic turn is system-generated input\n * — neither is the reader speaking, so neither gets the prompt frame or counts as a stop.\n */\nexport const isPrompt = (message: Message.Message): boolean =>\n message.sender.role === 'user' &&\n message.blocks.some((block) => block._tag === 'text' && block.disposition !== 'synthetic');\n\n/** Plain text of a message, used by search and copy so both read the same projection. */\nexport const messageText = (message: Message.Message, renderer: MessageRenderer): string => {\n const content = renderer(message);\n switch (content.kind) {\n case 'markdown':\n return content.text;\n case 'html':\n return stripHtml(content.html);\n case 'custom':\n return '';\n }\n};\n\n/** Crude tag strip — the engine only needs searchable/copyable text, not a parse tree. */\nconst stripHtml = (html: string): string =>\n html\n .replace(/<[^>]*>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n\n//\n// Search\n//\n\nexport type SearchHit = {\n messageId: string;\n /** Index into the message list, so the virtualizer can scroll to it without a second lookup. */\n index: number;\n /** Offset into the message's rendered text. */\n offset: number;\n length: number;\n};\n\n/**\n * Search the model rather than the DOM.\n *\n * Virtualization means most messages are not mounted, and a `viewType` filter can hide blocks that\n * are still in the model — neither is reachable by a per-view CodeMirror search, so thread-wide find\n * has to run over the projection.\n */\nexport const searchFeed = (\n messages: readonly Message.Message[],\n renderer: MessageRenderer,\n query: string,\n): SearchHit[] => {\n const hits: SearchHit[] = [];\n if (!query.length) {\n return hits;\n }\n\n const needle = query.toLowerCase();\n messages.forEach((message, index) => {\n const text = messageText(message, renderer).toLowerCase();\n let offset = text.indexOf(needle);\n while (offset !== -1) {\n hits.push({ messageId: message.id, index, offset, length: query.length });\n offset = text.indexOf(needle, offset + needle.length);\n }\n });\n\n return hits;\n};\n\n//\n// Selection\n//\n\n/** A point in the feed: a message plus an offset into its rendered text. */\nexport type FeedAnchor = { messageId: string; offset: number };\n\nexport type FeedRange = { from: FeedAnchor; to: FeedAnchor };\n\n/**\n * Text between two anchors, spanning message boundaries.\n *\n * Native selection cannot span separate editor instances, and unmounted messages are not in the DOM\n * at all, so cross-message copy is reconstructed from the model.\n */\nexport const sliceFeed = (\n messages: readonly Message.Message[],\n renderer: MessageRenderer,\n range: FeedRange,\n): string => {\n const fromIndex = messages.findIndex((message) => message.id === range.from.messageId);\n const toIndex = messages.findIndex((message) => message.id === range.to.messageId);\n if (fromIndex === -1 || toIndex === -1) {\n return '';\n }\n\n // Normalize so a backwards drag (focus before anchor) yields the same text as a forwards one.\n const [start, end] =\n fromIndex < toIndex || (fromIndex === toIndex && range.from.offset <= range.to.offset)\n ? [range.from, range.to]\n : [range.to, range.from];\n\n const [startIndex, endIndex] = fromIndex < toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex];\n\n const parts: string[] = [];\n for (let index = startIndex; index <= endIndex; index++) {\n const text = messageText(messages[index], renderer);\n const from = index === startIndex ? start.offset : 0;\n const to = index === endIndex ? end.offset : text.length;\n parts.push(text.slice(from, to));\n }\n\n return parts.join('\\n\\n');\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { ListModel } from '@dxos/react-ui-virtual';\nimport { type Message } from '@dxos/types';\n\nimport {\n type MessageRenderer,\n type SearchHit,\n defaultRenderer,\n isPrompt,\n messageText,\n searchFeed,\n} from './feed-model.ts';\n\n/**\n * One position navigation can land on (SPEC: Stop).\n *\n * Produced by a pluggable policy over the model and consumed identically by every driver —\n * toolbar, arrow keys, outliner, minimap — which is what keeps the rails and the keymap from\n * growing parallel wiring.\n */\nexport type Stop = {\n index: number;\n id: string;\n label?: string;\n};\n\n/** The built-in policies; a host may hand a function instead. */\nexport type StopsPolicy = 'message' | 'prompt' | ((message: Message.Message, index: number) => boolean);\n\nexport type FeedModelOptions = {\n messages?: readonly Message.Message[];\n stops?: StopsPolicy;\n /** Paged history: consumed when the window reaches the start edge. Paging is the model's business. */\n loadBefore?: () => Promise<readonly Message.Message[]>;\n /** The forward twin, for feeds that are windows into something longer. */\n loadAfter?: () => Promise<readonly Message.Message[]>;\n};\n\n/**\n * The native case, not an add-on (SPEC: FeedModel): a ListModel of messages carrying the feed's\n * own semantics — the stops policy, the streaming tail, and iteration. The list consumes this and\n * nothing else; `hits`, `isAnchor` and `streamingId` all left the props API and live here or in\n * the decoration provider.\n *\n * ECHO/queue bindings adapt to this outside the package; `fromMessages` adapts the plain case.\n */\nexport class FeedModel extends ListModel<Message.Message> {\n #stops: StopsPolicy;\n #streamingId: string | undefined;\n #loading = false;\n readonly #loadBefore?: () => Promise<readonly Message.Message[]>;\n readonly #loadAfter?: () => Promise<readonly Message.Message[]>;\n\n constructor({ messages = [], stops = 'message', loadBefore, loadAfter }: FeedModelOptions = {}) {\n super({ items: messages, getId: (message) => message.id });\n this.#stops = stops;\n this.#loadBefore = loadBefore;\n this.#loadAfter = loadAfter;\n }\n\n get messages(): readonly Message.Message[] {\n return this.items;\n }\n\n /** The tail message reconciling by delta; its item must not remount per chunk. */\n get streamingId(): string | undefined {\n return this.#streamingId;\n }\n\n setStreaming(id: string | undefined): void {\n const previous = this.#streamingId;\n if (previous === id) {\n return;\n }\n this.#streamingId = id;\n // The item that stopped (or started) streaming re-renders; the rest of the feed does not care.\n for (const changed of [previous, id]) {\n if (changed) {\n this.update(changed);\n }\n }\n }\n\n /** A chunk arrived for the streaming tail: same identity, longer content. */\n stream(id: string): void {\n this.update(id);\n }\n\n setStops(stops: StopsPolicy): void {\n this.#stops = stops;\n }\n\n /**\n * The positions navigation can land on, under the current policy.\n *\n * Recomputed on demand rather than cached: a policy is a predicate over the model, and the model\n * knows when it changed — callers that want reactivity derive from `rowsAtom`.\n */\n stops(): Stop[] {\n const predicate = this.#stops === 'message' ? () => true : this.#stops === 'prompt' ? isPrompt : this.#stops;\n\n const stops: Stop[] = [];\n this.messages.forEach((message, index) => {\n if (predicate(message, index)) {\n stops.push({ index, id: message.id });\n }\n });\n\n return stops;\n }\n\n /**\n * The window reached the start: ask the source for history.\n *\n * The model owns iteration (SPEC F-7.2) — the pages arrive as a prepend, which the virtualizer\n * is told about, so nothing on screen moves (F-1.1). Serialized, because edges are reached once\n * per frame while the reader sits on them.\n */\n async more(edge: 'start' | 'end'): Promise<boolean> {\n const load = edge === 'start' ? this.#loadBefore : this.#loadAfter;\n if (!load || this.#loading) {\n return false;\n }\n\n this.#loading = true;\n try {\n const page = await load();\n if (!page.length) {\n return false;\n }\n\n if (edge === 'start') {\n this.prepend(page);\n } else {\n this.append(page);\n }\n\n return true;\n } finally {\n this.#loading = false;\n }\n }\n\n /** Model-level search, since virtualization hides most items from any DOM search. */\n search(query: string, renderer: MessageRenderer = defaultRenderer): SearchHit[] {\n return searchFeed(this.messages, renderer, query);\n }\n\n /** The rendered text of one message, for previews and rails. */\n textOf(id: string, renderer: MessageRenderer = defaultRenderer): string | undefined {\n const index = this.indexOf(id);\n const message = this.at(index);\n return message && messageText(message, renderer);\n }\n}\n\n/** The plain case: a feed over an array the host already has. */\nexport const fromMessages = (messages: readonly Message.Message[], options: Omit<FeedModelOptions, 'messages'> = {}) =>\n new FeedModel({ messages, ...options });\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { useEffect, useMemo } from 'react';\n\nimport { type Message } from '@dxos/types';\n\nimport { FeedModel, type FeedModelOptions } from './feed.ts';\n\n/**\n * A feed model over a message array the host re-creates per render — the React-prop case.\n *\n * The model is stable; the array is folded into it with `replace`, which is where the prepend\n * inference lives (and the only place — SPEC F-7.1). In an effect rather than during render: the\n * model publishes synchronously to its subscribers, and a subscriber invalidating itself while the\n * host is still rendering is a cross-component update React rejects. The window it opens is one\n * frame, and the window's own subscription closes it.\n *\n * Streaming through this adapter is a `replace` whose length does not change; the model still\n * publishes, so the window still re-renders, and the streaming item reconciles by document delta\n * exactly as it would under a told update.\n */\nexport const useFeedModel = (\n messages: readonly Message.Message[],\n options: Omit<FeedModelOptions, 'messages'> = {},\n): FeedModel => {\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const model = useMemo(() => new FeedModel({ messages, ...options }), []);\n useEffect(() => {\n if (model.messages !== messages) {\n model.replace(messages);\n }\n }, [model, messages]);\n\n return model;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n/** Movement below this is sub-pixel rounding, not a row changing place. */\nconst EPSILON = 1;\n\nexport type PositionStats = {\n /** Rows whose offset changed after they were laid out — every one of these is a visible flicker. */\n shifts: number;\n /** Windows where offsets did not increase with index; a row was placed over its neighbour. */\n breaks: number;\n /** The most recent shift, for reading in the console while scrolling. */\n last?: PositionShift;\n};\n\nexport type PositionShift = { index: number; from: number; to: number; delta: number };\n\n/** Recent shifts kept for inspection — enough to see whether one row type moves or all of them. */\nconst TRACE_LIMIT = 40;\n\n/** `key` is the virtualizer's item key — the message id, via `getItemKey`. */\nexport type PositionKey = number | string | bigint;\n\nexport type PositionEntry = { key: PositionKey; index: number; start: number };\n\n/** Movement of the viewport itself between samples, which every row is expected to follow. */\nexport type PositionFrame = { scrollOffset: number };\n\nconst EMPTY: PositionStats = { shifts: 0, breaks: 0 };\n\n/**\n * Records where each row was placed, and reports when a row moves.\n *\n * A virtualized list is only still if a row's offset, once laid out, does not change: rows above the\n * reader are measured as they mount, and any correction there pushes everything below it. Scrolling\n * **up** is where this shows, because that is the direction in which unmeasured rows enter — going\n * down, the rows above have already been measured.\n *\n * Positions are recorded **relative to the viewport** (`start - scrollOffset`), not as absolute\n * document offsets, because those two answer different questions. Measuring a row for the first time\n * re-lays every row below it, and the virtualizer compensates by moving `scrollTop` the same\n * distance — the offsets all change and the reader sees nothing. What a reader sees is a row landing\n * somewhere else on screen, which is what this counts.\n *\n * Two invariants:\n *\n * 1. a row keyed by message id holds its place on screen for as long as it stays mounted;\n * 2. positions within the mounted window increase strictly with index.\n *\n * The count is what a reader means by \"flicker\". Deliberately kept as a count rather than a log:\n * a scroll produces thousands of samples, and the number is the signal.\n */\n/**\n * Watches for jumps in what was actually painted, once per animation frame.\n *\n * The counting above is done per React render, which cannot separate a row moving from the sampling\n * happening at a different moment than the layout. This reads the scroll and every row's box in the\n * **same** frame and compares each row's travel against the scroll's: a row that keeps its place in\n * the document travels exactly as far as the scroll did, so anything else is the reader seeing a\n * jump. It is the in-page form of measuring a screen recording frame by frame, and it is what a\n * developer should have running while working on the list.\n */\nexport const useJumpDetector = (viewport: HTMLElement | null, enabled = true) => {\n const [jumps, setJumps] = useState({ count: 0, worst: 0 });\n\n useEffect(() => {\n if (!viewport || !enabled) {\n return;\n }\n\n let handle = 0;\n let previous = new Map<string, number>();\n let previousScroll = viewport.scrollTop;\n let count = 0;\n let worst = 0;\n\n const tick = () => {\n const scroll = viewport.scrollTop;\n const travelled = previousScroll - scroll;\n const current = new Map<string, number>();\n for (const row of viewport.querySelectorAll<HTMLElement>('[data-index]')) {\n const top = row.getBoundingClientRect().top;\n const index = row.dataset.index!;\n current.set(index, top);\n const before = previous.get(index);\n if (before !== undefined) {\n const residual = Math.abs(top - (before + travelled));\n if (residual > EPSILON) {\n count++;\n worst = Math.max(worst, Math.round(residual));\n }\n }\n }\n\n previous = current;\n previousScroll = scroll;\n handle = requestAnimationFrame(tick);\n };\n\n handle = requestAnimationFrame(tick);\n const timer = setInterval(() => setJumps({ count, worst }), 250);\n return () => {\n cancelAnimationFrame(handle);\n clearInterval(timer);\n };\n }, [viewport, enabled]);\n\n return jumps;\n};\n\nexport const usePositionLog = () => {\n const positions = useRef(new Map<PositionKey, number>());\n const trace = useRef<PositionShift[]>([]);\n const [stats, setStats] = useState<PositionStats>(EMPTY);\n const pending = useRef<PositionStats>(EMPTY);\n\n const reset = useCallback(() => {\n positions.current.clear();\n trace.current = [];\n pending.current = EMPTY;\n setStats(EMPTY);\n }, []);\n\n const record = useCallback((entries: readonly PositionEntry[], { scrollOffset }: PositionFrame) => {\n let { shifts, breaks, last } = pending.current;\n\n let previousStart = -Infinity;\n for (const { key, index, start: absolute } of entries) {\n const start = absolute - scrollOffset;\n const recorded = positions.current.get(key);\n if (recorded !== undefined && Math.abs(recorded - start) > EPSILON) {\n shifts++;\n last = { index, from: Math.round(recorded), to: Math.round(start), delta: Math.round(start - recorded) };\n trace.current.push(last);\n if (trace.current.length > TRACE_LIMIT) {\n trace.current.shift();\n }\n }\n\n positions.current.set(key, start);\n if (start < previousStart) {\n breaks++;\n }\n previousStart = start;\n }\n\n // Rows that are no longer mounted are forgotten: their next measurement is a fresh layout, and\n // holding the old offset would report every remount as a shift.\n const mounted = new Set(entries.map(({ key }) => key));\n for (const key of positions.current.keys()) {\n if (!mounted.has(key)) {\n positions.current.delete(key);\n }\n }\n\n const next = { shifts, breaks, last };\n pending.current = next;\n setStats((previous) =>\n previous.shifts === next.shifts && previous.breaks === next.breaks ? previous : { ...next },\n );\n }, []);\n\n return { ...stats, trace, record, reset };\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, {\n type ComponentProps,\n type ComponentType,\n type PropsWithChildren,\n type ReactNode,\n type Ref,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { type WindowController, type WindowState, useFollow, useWindow, windowRowProps } from '@dxos/react-ui-virtual';\nimport * as Button from '@dxos/react-ui/Button';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as ScrollArea from '@dxos/react-ui/ScrollArea';\nimport * as Util from '@dxos/react-ui/Util';\nimport { type Message } from '@dxos/types';\nimport { type ObjectLinkProps, type WidgetDef, type XmlWidgetRegistry } from '@dxos/ui-editor';\n\nimport { type FeedNavigation, useDecorations, useFeedNavigation, useItemSelectionValue } from '../../hooks/index.ts';\nimport { type FeedModel, type ItemContent, type MessageRenderer, defaultRenderer } from '../../model/index.ts';\nimport {\n type HighlightRange,\n HtmlBlock,\n MarkdownBlock,\n SelectionGroupContext,\n WidgetScopeProvider,\n WidgetStateProvider,\n createSelectionGroup,\n createWidgetStateStore,\n} from '../Block/index.ts';\nimport { useJumpDetector, usePositionLog } from './position-log.ts';\n\n//\n// Context\n//\n\ntype ScrollAreaRootProps = ComponentProps<typeof ScrollArea.Root>;\n\nconst MESSAGE_LIST_NAME = 'MessageList';\n\n/** Per-message chrome (avatar, timestamp, fork/rewind/reply controls), supplied by the host. */\nexport type MessageChromeProps = PropsWithChildren<{\n message: Message.Message;\n index: number;\n selected: boolean;\n onSelect: (id: string, additive: boolean) => void;\n}>;\n\nexport type MessageRange = { startIndex: number; endIndex: number };\n\n/**\n * The imperative handle a host outside the feed's context drives the list through — the successor\n * of a document controller, shrunk to what a message-indexed feed needs. Everything else (range,\n * census, debug) is context, read via {@link useMessageList}.\n */\nexport type MessageListController = {\n model: FeedModel;\n scrollToBottom: (options?: ScrollToOptions) => void;\n scrollToIndex: (index: number, options?: ScrollToOptions) => void;\n /** The one seam every navigation driver shares: toolbar, arrows, rails (SPEC F-3.2). */\n navigation: FeedNavigation;\n};\n\ntype MessageListContextValue = {\n /** The feed's model: messages, identity, stops, streaming, iteration. The one source (SPEC F-7). */\n model: FeedModel;\n renderer: MessageRenderer;\n registry?: XmlWidgetRegistry;\n /** The block widget for an object embedded as a card (`![label](echo://…)`); the host supplies one that can render a card. */\n objectImage?: WidgetDef<ObjectLinkProps>;\n Chrome: ComponentType<MessageChromeProps>;\n Custom?: ComponentType<{ content: ItemContent & { kind: 'custom' }; message: Message.Message }>;\n debug?: boolean;\n /** The mounted window; `undefined` until the viewport has measured. */\n range?: MessageRange;\n /** Index the reader is on: moved by the arrow keys and by any navigation, tracked while scrolling. */\n currentIndex: number;\n /** Whether the reader is resting on the tail — what a scroll-to-bottom affordance hides against. */\n atEnd: boolean;\n /** Whether the list is keeping the tail in view itself; an affordance to return there has no place while it is. */\n following: boolean;\n /** The one seam every navigation driver calls: toolbar, arrows, outline, minimap (SPEC F-3.2). */\n navigation: FeedNavigation;\n /** Rows mounted right now: the window the reader is paying for. */\n mountedRows: number;\n /** Block widgets mounted across every mounted item — what the visible window actually costs. */\n mountedWidgets: number;\n reportWidgets: (id: string, count: number) => void;\n /** Rows that moved on screen against the scroll, counted per animation frame. */\n jumps: { count: number; worst: number };\n /** Rows that moved after being laid out, and windows whose offsets were out of order. */\n shifts: number;\n breaks: number;\n resetShifts: () => void;\n /** The element holding the mounted rows, which is what gets measured. */\n windowRef: React.RefObject<HTMLDivElement | null>;\n /** Absolute position of the first mounted row: what that element is translated by (§7). */\n offset: number;\n /** Extent of the whole document, reserved space included — what the thumb is scaled against. */\n sizerExtent: number;\n first: number;\n last: number;\n setViewport: (viewport: HTMLElement | null) => void;\n /** How many times the model has said this message changed in place. */\n revisionOf: (id: string) => number;\n scrollToIndex: (index: number, options?: ScrollToOptions) => void;\n scrollToBottom: (options?: ScrollToOptions) => void;\n};\n\nexport type ScrollToOptions = {\n /** The alignments the virtualizer honours; centering is a host concern it does not have. */\n align?: 'start' | 'end';\n behavior?: 'auto' | 'smooth';\n};\n\nconst [MessageListProvider, useMessageListContext] = Hooks.createContext<MessageListContextValue>(MESSAGE_LIST_NAME);\n\n/**\n * What an item renders with — split from the list's context because that one changes on every\n * scroll (range, offset, cursor), and a consumer re-renders whenever its context does: every mounted\n * row would be rebuilt per frame of a scroll that moved none of them.\n */\ntype MessageListItemContextValue = Pick<\n MessageListContextValue,\n 'model' | 'renderer' | 'registry' | 'objectImage' | 'Custom' | 'debug' | 'reportWidgets'\n>;\n\nconst [MessageListItemProvider, useMessageListItemContext] =\n Hooks.createContext<MessageListItemContextValue>(MESSAGE_LIST_NAME);\n\n/**\n * The list's state and scroll controls, for parts that live outside the viewport — a toolbar's\n * find-next, a statusbar's range readout.\n */\nexport const useMessageList = (consumerName = 'useMessageList') => {\n const {\n range,\n currentIndex,\n atEnd,\n following,\n navigation,\n mountedRows,\n mountedWidgets,\n jumps,\n shifts,\n breaks,\n resetShifts,\n model,\n scrollToIndex,\n scrollToBottom,\n } = useMessageListContext(consumerName);\n return {\n range,\n currentIndex,\n atEnd,\n following,\n navigation,\n mountedRows,\n mountedWidgets,\n jumps,\n shifts,\n breaks,\n resetShifts,\n model,\n count: model.count,\n scrollToIndex,\n scrollToBottom,\n };\n};\n\n//\n// Root\n//\n\nexport type MessageListRootProps = PropsWithChildren<{\n /**\n * The feed's model (SPEC F-7): messages and identity, the stops policy, the streaming tail, and\n * iteration (`loadBefore`/`loadAfter`, consumed when the window reaches an edge). `fromMessages`\n * adapts a plain array; ECHO/queue bindings adapt outside this package.\n *\n * What is NOT here, deliberately: search hits (decorations — `DecorationsProvider` + an item's\n * `useDecorations`), selection (`ItemSelectionProvider` + `useItemSelection`), `streamingId` and\n * `isAnchor` (model concerns). The list neither knows nor routes cross-cutting per-item data.\n */\n model: FeedModel;\n renderer?: MessageRenderer;\n registry?: XmlWidgetRegistry;\n /** The block widget for an object embedded as a card (`![label](echo://…)`). */\n objectImage?: WidgetDef<ObjectLinkProps>;\n /**\n * Chrome wrapper; receives the item as `children`. Defaults to a bare frame.\n * Chrome must be layout-stable: a control that changes a row's height on hover or focus\n * re-triggers measurement, and a pointer travelling down the list mid-scroll then shifts every\n * row below it. Toggle such affordances with opacity, or take them out of flow.\n */\n Chrome?: ComponentType<MessageChromeProps>;\n /**\n * Renders a `custom` item. The engine's own kinds are documents — markdown in CodeMirror, HTML —\n * and both build their content asynchronously enough to be measured wrong once. A host component\n * whose height is settled at first paint is the control case: if a feed of those still moves, the\n * fault is the list; if it does not, the fault is in what the item is building.\n */\n Custom?: ComponentType<{ content: ItemContent & { kind: 'custom' }; message: Message.Message }>;\n /**\n * Outline each item and the blocks inside it, so what the layout is measuring is visible.\n */\n debug?: boolean;\n /**\n * Estimated row height before measurement. A function is worth supplying whenever the feed's\n * rows differ widely — a one-line prompt beside a long answer — because a single number is wrong\n * for both.\n */\n estimateSize?: number | ((message: Message.Message, index: number) => number);\n /**\n * Pin to the bottom as messages arrive, and as a streaming message grows (chat behaviour).\n */\n stickyBottom?: boolean;\n /**\n * Blank lines kept below the last row, part of the resting view: the tail sits that much clear\n * of the viewport's end (breathing room above a composer). A constant number in the sizer —\n * lines × the viewport's line-height — not a mode the list has (§7).\n */\n tailLines?: number;\n overscan?: number;\n onRangeChange?: (range: MessageRange) => void;\n /** Published once the list is wired; for hosts composing chrome outside the feed's context. */\n controllerRef?: Ref<MessageListController>;\n}>;\n\nconst DefaultChrome = ({ children }: MessageChromeProps) => <>{children}</>;\n\n/** Row height assumed before anything has been measured. */\nconst DEFAULT_ESTIMATE = 120;\n\n/**\n * Headless root of a feed: owns the placement, the selection group and the model-to-item mapping.\n * Renders no DOM of its own, so a toolbar or statusbar that reads its state can sit outside the\n * scroll container — the scrolling part is `MessageList.Viewport`.\n */\nconst MessageListRoot = ({\n children,\n model,\n renderer = defaultRenderer,\n registry,\n objectImage,\n Chrome = DefaultChrome,\n Custom,\n debug,\n estimateSize = DEFAULT_ESTIMATE,\n stickyBottom = false,\n tailLines = 0,\n overscan = 8,\n onRangeChange,\n controllerRef,\n}: MessageListRootProps) => {\n // Owned here so it outlives every row: a widget's state must survive its row being destroyed.\n const widgetStore = useMemo(createWidgetStateStore, []);\n\n const [viewport, setViewportState] = useState<HTMLElement | null>(null);\n // The same element as a ref, because the placement is bound to an element rather than to a render.\n const scrollerRef = useRef<HTMLElement | null>(null);\n const setViewport = useCallback((element: HTMLElement | null) => {\n scrollerRef.current = element;\n setViewportState(element);\n }, []);\n\n const [range, setRange] = useState<MessageRange | undefined>(undefined);\n const [mounted, setMounted] = useState(0);\n\n // One number standing in for the caller's estimate wherever a single value is needed, since a\n // per-row estimator answers about a row and not about the feed.\n const nominalSize = typeof estimateSize === 'number' ? estimateSize : DEFAULT_ESTIMATE;\n\n const [currentIndex, setCurrentIndex] = useState(0);\n const currentIndexRef = useRef(0);\n const setCurrent = useCallback((index: number) => {\n currentIndexRef.current = index;\n setCurrentIndex(index);\n }, []);\n\n // Widget census, kept per item so an item that unmounts takes its own widgets out of the total.\n // Batched into state rather than counted on every report: a scroll mounts and unmounts several\n // items per frame, and a readout is not worth a render each.\n const widgetCounts = useRef(new Map<string, number>());\n const [mountedWidgets, setMountedWidgets] = useState(0);\n const reportWidgets = useCallback((id: string, count: number) => {\n if (count) {\n widgetCounts.current.set(id, count);\n } else {\n widgetCounts.current.delete(id);\n }\n\n setMountedWidgets([...widgetCounts.current.values()].reduce((total, value) => total + value, 0));\n }, []);\n\n // What the host says, and nothing else.\n //\n // There is no running average here and no re-base built on one. Both existed because the old\n // engine could only revise the rows below the last one it had measured, so a feed opened at its\n // tail kept a document sized by the opening guess and had to be rebuilt from the top to correct\n // it — which moved every offset in the list at once. A measurement is kept against the row's id\n // and consulted from the anchor, so a wrong estimate is wrong only for rows nobody has seen (§8).\n const extents = useMemo(\n () => ({\n of: (index: number) => {\n const message = model.at(index);\n return typeof estimateSize === 'function' && message ? estimateSize(message, index) : nominalSize;\n },\n }),\n [model, estimateSize, nominalSize],\n );\n\n // A constant input to the sizer: lines × the viewport's line-height, read once per mount. The\n // previous design derived a viewport-sized reserve from the tail's measured extents (so the last\n // prompt could reach the top), which was several moving parts — measurements, the stops policy,\n // a freshness freeze — and never reliable. A constant cannot oscillate.\n const [reserve, setReserve] = useState(0);\n useEffect(() => {\n if (!viewport || !tailLines) {\n setReserve(0);\n return;\n }\n\n const lineHeight = parseFloat(getComputedStyle(viewport).lineHeight) || 24;\n setReserve(Math.round(tailLines * lineHeight));\n }, [viewport, tailLines]);\n\n const controller = useRef<WindowController>(null);\n const onWindowChange = useCallback(\n (state: WindowState) => {\n const next = state.count ? { startIndex: state.visible.first, endIndex: state.visible.last } : undefined;\n setRange(next);\n setMounted(state.mounted.last - state.mounted.first + 1);\n if (next) {\n onRangeChange?.(next);\n // The cursor is the row at the top of the viewport, derived from the scroll and never set\n // alongside it. That is what makes a press always move: the row containing the offset is the\n // one the stop above begins strictly higher than, and the stop below strictly lower.\n setCurrent(next.startIndex);\n\n // Iteration is the model's (SPEC F-7.2): the window reaching an edge asks it for more, the\n // page arrives as a prepend the anchor is told about, and nothing on screen moves. The model\n // serializes and drains, so asking every commit costs nothing once the source is done.\n if (next.startIndex === 0) {\n void model.more('start');\n }\n if (next.endIndex >= model.count - 1) {\n void model.more('end');\n }\n }\n },\n [onRangeChange, setCurrent, model],\n );\n\n // The model *is* the window's model: FeedModel satisfies the structural contract, so there is no\n // adapter and no second copy of the truth.\n const { placement, windowRef, offset, sizerExtent, first, last } = useWindow({\n scrollerRef,\n model,\n extents,\n overscan,\n reserve,\n onChange: onWindowChange,\n controllerRef: controller,\n });\n\n // When the content last actually changed, as against merely re-measured: what the follow is\n // allowed to chase (a toggle near the tail grows the document too, and is the reader's business).\n const changedAtRef = useRef(performance.now());\n useEffect(() => model.subscribe(() => (changedAtRef.current = performance.now())), [model]);\n\n // Per-message revision, bumped when the model says a message changed in place: a streamed message\n // keeps its identity, so a memoized row cannot tell it changed from the message alone.\n const revisions = useRef(new Map<string, number>());\n useEffect(\n () =>\n model.subscribe(({ updated }) => {\n for (const id of updated ?? []) {\n revisions.current.set(id, (revisions.current.get(id) ?? 0) + 1);\n }\n }),\n [model],\n );\n const revisionOf = useCallback((id: string) => revisions.current.get(id) ?? 0, []);\n\n // The follow is an aspect the host composes, not part of the window (SPEC §Aspects) — the intent,\n // its withdrawal by a backwards scroll, and the glide all live there.\n const follow = useFollow({\n scrollerRef,\n placement,\n extent: sizerExtent,\n count: model.count,\n reserve,\n enabled: stickyBottom,\n changedAt: () => changedAtRef.current,\n });\n\n // What a reader would call flicker: a row moving on screen by more than the scroll moved, sampled\n // once per frame so the reading is of what was painted. Only while debugging — it reads every\n // mounted row's box every frame.\n const jumps = useJumpDetector(viewport, debug);\n\n // Where each row was placed, and whether it stayed there.\n const { shifts, breaks, trace: shiftTrace, record: recordPositions, reset: resetShifts } = usePositionLog();\n useEffect(() => {\n const rows = [];\n for (let index = first; index <= last; index++) {\n rows.push({ key: model.getId(index), index, start: placement.positionOf(index) });\n }\n\n // The offset the layout was computed against, not the element's current `scrollTop`: reading the\n // DOM here samples a scroll that has moved on since, and every row then looks displaced by\n // exactly one frame of travel.\n recordPositions(rows, { scrollOffset: placement.scroll });\n }, [first, last, offset, sizerExtent, model, placement, recordPositions]);\n\n const scrollToIndex = useCallback(\n (index: number, { align = 'start', behavior = 'auto' }: ScrollToOptions = {}) => {\n // Before the write, not via the scroll event it raises: asking to be somewhere answers \"do\n // you want the tail?\", and the correction effect must not get a word in first.\n follow.onNavigate(index);\n controller.current?.scrollToIndex(index, align, behavior);\n },\n [follow],\n );\n\n const scrollToBottom = useCallback(\n (options?: ScrollToOptions) => {\n if (!model.count) {\n return;\n }\n\n // Already at rest: arm the follow and do nothing else — the jump re-anchors from estimates,\n // which visibly moves a reader who has not moved (seen on submit while pinned).\n const scroller = scrollerRef.current;\n if (scroller && scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop <= 32) {\n follow.onNavigate(model.count - 1);\n return;\n }\n\n // The end includes the reserve: the tail rests its tail-lines clear of the viewport's edge.\n scrollToIndex(model.count - 1, { align: 'end', ...options });\n },\n [scrollToIndex, model, follow],\n );\n\n useEffect(() => {\n if (viewport) {\n // Why the list is where it is, readable from the console as `$0.__feed`.\n (viewport as any).__feed = {\n placement,\n get layout() {\n return placement.layout();\n },\n get shifted() {\n return { shifts, breaks, recent: shiftTrace.current };\n },\n };\n }\n }, [viewport, placement, shifts, breaks, shiftTrace]);\n\n // The one seam every driver calls (SPEC F-3.2): the toolbar's buttons, the arrows below, and the\n // rails all step through this, over the stops the model's policy names (SPEC F-3.1).\n const navigation = useFeedNavigation({\n controller,\n stops: () => model.stops(),\n current: () => currentIndexRef.current,\n count: () => model.count,\n // The follow scrolls only while content arrives AND the reader is pinned to the bottom;\n // navigating anywhere but the last row un-pins, before the jump's own scroll event fires.\n onNavigate: follow.onNavigate,\n });\n\n // Arrow keys move by message, not by line: a plain arrow steps the cursor to the adjacent message\n // and Cmd/Ctrl + Arrow jumps to the first or last.\n //\n // Bound imperatively to the viewport element (rather than as a React prop) because\n // `ScrollArea.Viewport` narrows its props to the slottable set, and wrapping it in a focusable div\n // would insert a box into the height chain.\n useEffect(() => {\n if (!viewport) {\n return;\n }\n\n // The items are non-editable, so nothing inside the list takes focus on its own; the viewport\n // has to be focusable for the keymap to have somewhere to land.\n viewport.tabIndex = 0;\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.altKey || event.shiftKey) {\n return;\n }\n\n const delta = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;\n if (!delta) {\n return;\n }\n\n event.preventDefault();\n if (event.metaKey || event.ctrlKey) {\n if (delta > 0) {\n navigation.last();\n } else {\n navigation.first();\n }\n return;\n }\n\n navigation.step(delta);\n };\n\n viewport.addEventListener('keydown', onKeyDown);\n return () => viewport.removeEventListener('keydown', onKeyDown);\n }, [viewport, navigation]);\n\n // The feed has one selection even though it has many editors; the group is what enforces that.\n const selectionGroup = useMemo(createSelectionGroup, []);\n\n // The parts are stable identities, so this republishes only when the model itself is replaced.\n useEffect(() => {\n Hooks.setRef(controllerRef, { model, scrollToBottom, scrollToIndex, navigation });\n return () => {\n Hooks.setRef(controllerRef, null);\n };\n }, [controllerRef, model, scrollToBottom, scrollToIndex, navigation]);\n\n return (\n <SelectionGroupContext.Provider value={selectionGroup}>\n <WidgetStateProvider store={widgetStore}>\n <MessageListProvider\n model={model}\n renderer={renderer}\n registry={registry}\n objectImage={objectImage}\n Chrome={Chrome}\n Custom={Custom}\n debug={debug}\n range={range}\n currentIndex={currentIndex}\n atEnd={follow.atEnd}\n following={follow.following}\n navigation={navigation}\n mountedRows={mounted}\n mountedWidgets={mountedWidgets}\n reportWidgets={reportWidgets}\n jumps={jumps}\n shifts={shifts}\n breaks={breaks}\n resetShifts={resetShifts}\n windowRef={windowRef}\n offset={offset}\n sizerExtent={sizerExtent}\n first={first}\n last={last}\n setViewport={setViewport}\n revisionOf={revisionOf}\n scrollToIndex={scrollToIndex}\n scrollToBottom={scrollToBottom}\n >\n <MessageListItemProvider\n model={model}\n renderer={renderer}\n registry={registry}\n objectImage={objectImage}\n Custom={Custom}\n debug={debug}\n reportWidgets={reportWidgets}\n >\n {children}\n </MessageListItemProvider>\n </MessageListProvider>\n </WidgetStateProvider>\n </SelectionGroupContext.Provider>\n );\n};\n\nMessageListRoot.displayName = 'MessageList.Root';\n\n//\n// Viewport\n//\n\nconst MESSAGE_LIST_VIEWPORT_NAME = 'MessageList.Viewport';\n\ntype MessageListViewportExtra = Pick<ScrollAreaRootProps, 'autoHide' | 'native' | 'scrollbars'> & {\n /** Inline gutter of each row, as a page Container's. */\n gutter?: 'sm' | 'md' | 'lg';\n /**\n * Chrome pinned over the scroller — a scroll-to-bottom pill, a \"new messages\" badge.\n *\n * Mounted inside `ScrollArea.Root` because it is positioned and does not scroll, so nothing has\n * to enter the flex-height chain the placement measures.\n */\n overlay?: ReactNode;\n};\n\n/**\n * The scroll container and the mounted window of rows.\n *\n * Scrolling belongs to `ScrollArea`, which owns the overlay thumbs and the padding tokens; the\n * placement needs only the element being scrolled, which `ScrollArea.Viewport` publishes.\n */\n/**\n * Whether a message renders to nothing under the current renderer — e.g. a stats-only turn, whose\n * block the renderer deliberately maps to no output. The message stays in the model (identity,\n * stops, search all see it); only its chrome is withheld, so the reader is not shown an empty row\n * with a toolbar.\n */\nconst isEmptyContent = (content: ItemContent, hasCustomRenderer: boolean): boolean =>\n (content.kind === 'markdown' && !content.text.trim()) ||\n (content.kind === 'html' && !content.html.trim()) ||\n (content.kind === 'custom' && !hasCustomRenderer);\n\nconst MessageListViewport = Util.composable<HTMLDivElement, MessageListViewportExtra>(\n ({ autoHide, native, scrollbars, gutter = 'md', overlay, ...props }, forwardedRef) => {\n const { model, renderer, Chrome, Custom, windowRef, offset, sizerExtent, first, last, setViewport, revisionOf } =\n useMessageListContext(MESSAGE_LIST_VIEWPORT_NAME);\n // The value once, per-row state derived: hooks do not run in loops, and the row loop below is\n // one. Item-shaped chrome uses `useItemSelection(id)` instead.\n const { selectedIds, onSelect } = useItemSelectionValue();\n\n const handleViewportRef = useCallback(\n (element: HTMLDivElement | null) => {\n setViewport(element);\n Hooks.setRef(forwardedRef, element);\n },\n [setViewport, forwardedRef],\n );\n\n // Stable, so a memoized row is not re-rendered for a callback that is new only by identity.\n const onSelectRef = useRef(onSelect);\n onSelectRef.current = onSelect;\n const handleSelect = useCallback((id: string, additive: boolean) => onSelectRef.current?.(id, additive), []);\n\n const rows = [];\n for (let index = first; index <= last; index++) {\n const message = model.at(index);\n if (!message) {\n continue;\n }\n\n rows.push(\n <MessageListRow\n key={message.id}\n message={message}\n revision={revisionOf(message.id)}\n index={index}\n // The row div always exists — the measurement pass walks the window's children by index —\n // but an empty render mounts no chrome inside it, so it measures at zero and takes no space.\n empty={isEmptyContent(renderer(message), Boolean(Custom))}\n selected={selectedIds?.has(message.id) ?? false}\n gutter={gutter}\n Chrome={Chrome}\n onSelect={handleSelect}\n />,\n );\n }\n\n return (\n <ScrollArea.Root\n {...Util.composableProps(props)}\n orientation='vertical'\n autoHide={autoHide}\n native={native}\n scrollbars={scrollbars}\n >\n <ScrollArea.Viewport\n data-testid='feed.viewport'\n // Off deliberately: the browser adjusting the scroll as well would be a second party\n // anchoring the same thing, and the defect this design exists to remove is exactly that.\n style={{ position: 'relative', overflowAnchor: 'none' }}\n classNames='dx-document'\n ref={handleViewportRef}\n >\n {/* Holds no rows: it exists only to give the thumb something to measure. */}\n <div style={{ height: sizerExtent }} data-testid='feed.sizer' />\n <div\n ref={windowRef}\n className='absolute top-0 left-0 flex flex-col w-full'\n style={{ transform: `translateY(${offset}px)` }}\n data-testid='feed.window'\n >\n {rows}\n </div>\n </ScrollArea.Viewport>\n {overlay}\n </ScrollArea.Root>\n );\n },\n);\n\nMessageListViewport.displayName = MESSAGE_LIST_VIEWPORT_NAME;\n\n//\n// Row\n//\n\ntype MessageListRowProps = {\n message: Message.Message;\n /** Stands in for the message's content: it changes when the model says the message changed in place. */\n revision: number;\n index: number;\n empty: boolean;\n selected: boolean;\n gutter: MessageListViewportExtra['gutter'];\n Chrome: ComponentType<MessageChromeProps>;\n onSelect: (id: string, additive: boolean) => void;\n};\n\n/**\n * One mounted row. Memoized because the window re-renders on every scroll event and after every\n * measurement, and nothing in a row depends on where the window is — only on its own props.\n */\nconst MessageListRow = memo(({ message, index, empty, selected, gutter, Chrome, onSelect }: MessageListRowProps) => (\n // Unpositioned, and that is the point. A row that changes extent reflows the ones after it,\n // in the browser, in the same frame; placing each row ourselves meant re-placing every row\n // below it on every frame of the change — 177 re-placements for one disclosure opening (§6).\n <div data-object-id={message.id} {...windowRowProps(index, message.id)}>\n {!empty && (\n <Layout.Container gutter={gutter}>\n {/* The widgets' query container: it must be an element whose width is definite, since\n containment stops a descendant's content sizing it (a prompt's bubble collapses). */}\n <div className='dx-container-type-inline-size'>\n <Chrome message={message} index={index} selected={selected} onSelect={onSelect}>\n <MessageListItem message={message} />\n </Chrome>\n </div>\n </Layout.Container>\n )}\n </div>\n));\n\nMessageListRow.displayName = 'MessageList.Row';\n\n//\n// Item\n//\n\nconst MESSAGE_LIST_ITEM_NAME = 'MessageList.Item';\n\ntype MessageListItemExtra = {\n message: Message.Message;\n};\n\n/**\n * One message, rendered by the kind its renderer resolves. Exposed so a host can render a message\n * outside the scrolling window — a pinned message, a preview — through the same path.\n */\nconst MessageListItem = Util.composable<HTMLDivElement, MessageListItemExtra>(({ message, ...props }, forwardedRef) => {\n const { model, renderer, registry, objectImage, Custom, debug, reportWidgets } =\n useMessageListItemContext(MESSAGE_LIST_ITEM_NAME);\n const content = renderer(message);\n // The item asks for its own cross-cutting data by id (SPEC §Aspects); the list never routed it.\n const decorations = useDecorations(message.id);\n const hits = useMemo<HighlightRange[] | undefined>(\n () =>\n decorations.length\n ? decorations.map(({ range }) => [range.offset, range.offset + range.length] as HighlightRange)\n : undefined,\n [decorations],\n );\n const handleWidgetsChange = useCallback(\n (count: number) => reportWidgets(message.id, count),\n [reportWidgets, message.id],\n );\n\n return (\n // The outlines are the item and the block-level children of its document — which is exactly what\n // the virtualizer measures and what a widget's late paint changes.\n <div\n {...Util.composableProps(props, {\n classNames: debug\n ? 'outline outline-1 outline-dashed outline-primary-500/50 [&_.cm-content>*]:outline [&_.cm-content>*]:outline-1 [&_.cm-content>*]:outline-dashed [&_.cm-content>*]:outline-neutral-500/40'\n : undefined,\n })}\n ref={forwardedRef}\n >\n {content.kind === 'markdown' && (\n <WidgetScopeProvider scope={message.id}>\n <MarkdownBlock\n text={content.text}\n // The typewriter runs only for the tail the model says is streaming; every other\n // change (an edit, a view switch) lands atomically.\n stream={model.streamingId === message.id}\n registry={registry}\n objectImage={objectImage}\n hits={hits}\n onWidgetsChange={handleWidgetsChange}\n />\n </WidgetScopeProvider>\n )}\n {content.kind === 'html' && <HtmlBlock html={content.html} />}\n {content.kind === 'custom' && Custom && <Custom content={content} message={message} />}\n </div>\n );\n});\n\nMessageListItem.displayName = MESSAGE_LIST_ITEM_NAME;\n\n//\n// Nav\n//\n\nconst MESSAGE_LIST_NAV_NAME = 'MessageList.Nav';\n\ntype MessageListNavExtra = {\n /** Render the jump-to-ends buttons as well as the steppers. @default true */\n ends?: boolean;\n};\n\n/**\n * The step and jump controls, on the same seam as the arrow keys and the rails (SPEC F-3.2), so a\n * reader who switches between them does not change places. Lives outside the viewport — Root is\n * headless precisely so chrome like this can sit in a toolbar.\n *\n * While any of its buttons has focus, ArrowUp/Down step and Meta(Ctrl)+Arrow jumps to the ends —\n * the same keymap the viewport itself carries, so focus landing on the toolbar does not change\n * what the keys mean.\n */\nconst MessageListNav = Util.composable<HTMLDivElement, MessageListNavExtra>(\n ({ ends = true, ...props }, forwardedRef) => {\n const { navigation } = useMessageListContext(MESSAGE_LIST_NAV_NAME);\n\n const onKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n const delta = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;\n if (!delta || event.altKey || event.shiftKey) {\n return;\n }\n\n event.preventDefault();\n if (event.metaKey || event.ctrlKey) {\n if (delta > 0) {\n navigation.last();\n } else {\n navigation.first();\n }\n return;\n }\n\n navigation.step(delta);\n },\n [navigation],\n );\n\n return (\n <div role='group' {...Util.composableProps(props)} onKeyDown={onKeyDown} ref={forwardedRef}>\n {ends && (\n <Button.Root\n icon='ph--arrow-line-up--regular'\n iconOnly\n label='First message'\n variant='ghost'\n data-testid='feed.nav.top'\n onClick={() => navigation.first()}\n />\n )}\n <Button.Root\n icon='ph--caret-up--regular'\n iconOnly\n label='Previous message'\n variant='ghost'\n data-testid='feed.nav.back'\n onClick={() => navigation.step(-1)}\n />\n <Button.Root\n icon='ph--caret-down--regular'\n iconOnly\n label='Next message'\n variant='ghost'\n data-testid='feed.nav.forward'\n onClick={() => navigation.step(1)}\n />\n {ends && (\n <Button.Root\n icon='ph--arrow-line-down--regular'\n iconOnly\n label='Last message'\n variant='ghost'\n data-testid='feed.nav.bottom'\n onClick={() => navigation.last()}\n />\n )}\n </div>\n );\n },\n);\n\nMessageListNav.displayName = MESSAGE_LIST_NAV_NAME;\n\n//\n// MessageList\n//\n\nexport const MessageList = {\n Root: MessageListRoot,\n Viewport: MessageListViewport,\n Item: MessageListItem,\n Nav: MessageListNav,\n};\n\nexport type {\n MessageListItemExtra as MessageListItemProps,\n MessageListNavExtra as MessageListNavProps,\n MessageListViewportExtra as MessageListViewportProps,\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React from 'react';\n\nimport { type WindowState } from '@dxos/react-ui-virtual';\nimport type * as Util from '@dxos/react-ui/Util';\nimport { mx } from '@dxos/ui-theme';\n\n/** 2rem, matching the outline rail it usually sits opposite. */\nconst DEFAULT_WIDTH = 32;\n\n/**\n * The whole list at a glance: what exists, what is mounted, and what the reader can see.\n *\n * Drawn in **content space**, not in indices. The three quantities a virtualized list can get wrong\n * are where the mounted rows are, how big the reader's slice of them is, and whether the total the\n * thumb is scaled against is honest — and none of those are visible from a row count. Here they are\n * three bars on one track, so a mounted range that has drifted away from the viewport, or a total\n * that jumps while nothing scrolls, is a picture rather than a number to be noticed.\n *\n * Debug only. It reads the state the window publishes and draws it (§2); it does not participate.\n */\nexport type MinimapProps = Util.ThemedClassName<{\n state?: WindowState;\n /** Width of the rail, in px. @default 32 (2rem) */\n width?: number;\n /**\n * Jump to where the reader pointed, as a fraction of the whole list.\n *\n * A fraction and not an index: the map is drawn in content space, so what a click names is a\n * position, and turning that into a row is the host's business — it has the count.\n */\n onSelect?: (fraction: number) => void;\n}>;\n\nexport const Minimap = ({ classNames, state, width = DEFAULT_WIDTH, onSelect }: MinimapProps) => {\n const total = state?.geometry.total ?? 0;\n const scale = (value: number) => (total > 0 ? `${Math.max(0, Math.min(100, (value / total) * 100))}%` : '0%');\n\n return (\n <div\n className={mx('relative h-full bg-input-surface overflow-hidden', onSelect && 'cursor-pointer', classNames)}\n style={{ width }}\n title={total ? `${Math.round(total)}px over ${state?.count} rows` : 'empty'}\n data-testid='minimap'\n onClick={\n onSelect &&\n ((event) => {\n const box = event.currentTarget.getBoundingClientRect();\n onSelect(Math.max(0, Math.min(1, (event.clientY - box.top) / box.height)));\n })\n }\n >\n {state && total > 0 && (\n <>\n {/* Mounted rows: where content actually exists. Should always contain the viewport — if it\n does not, the reader is looking at rows nobody has rendered. */}\n <div\n className='absolute inset-x-0 bg-accent-bg/30'\n style={{ top: scale(state.geometry.window.start), height: scale(state.geometry.window.extent) }}\n data-testid='minimap.mounted'\n />\n {/* Tenths, so a glance says roughly where in the list a bar sits without reading a number. */}\n {Array.from({ length: 9 }, (_, step) => (\n <div\n key={step}\n className='absolute inset-x-0 border-t border-separator/40'\n style={{ top: `${((step + 1) / 10) * 100}%` }}\n />\n ))}\n {/* The viewport, which is the reader. */}\n <div\n className='absolute inset-x-0 border-y border-accent-bg bg-accent-bg/70'\n style={{ top: scale(state.geometry.scroll), height: scale(state.geometry.viewport) }}\n data-testid='minimap.viewport'\n />\n </>\n )}\n </div>\n );\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, {\n type KeyboardEvent as ReactKeyboardEvent,\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as Popover from '@dxos/react-ui/Popover';\nimport type * as Util from '@dxos/react-ui/Util';\nimport { mx } from '@dxos/ui-theme';\n\n// Rest tick width (px) and the wave radius (in rows) over which the hover extension falls off. The\n// peak is the rail's own width: a tick at full extension spans it.\nconst REST_WIDTH = 8;\n/** 2rem, matching the debug minimap it usually sits opposite. */\nconst DEFAULT_WIDTH = 32;\nconst WAVE_SPREAD = 2;\n\n/** Height of one tick, and so the pitch the rail thins its markers to. */\nconst TICK_SIZE = 8;\n\nconst intersects = (range: { from: number; to: number }, visible: { from: number; to: number }): boolean =>\n range.from < visible.to && range.to > visible.from;\n\n/**\n * A single anchor marker within the mapped document.\n * `range` is expressed in the document's own position space (e.g. CodeMirror offsets).\n */\nexport type OutlineMarker = {\n id: string;\n title: string;\n description?: string;\n range: { from: number; to: number };\n};\n\nexport type OutlineProps = Util.ThemedClassName<{\n markers: OutlineMarker[];\n /** Currently-visible document range; markers intersecting it render brighter (\"active\"). */\n visibleRange?: { from: number; to: number };\n /**\n * Height of one tick, in px.\n *\n * Fixed rather than shared out over the rail's height: ticks that stretch to fill make a short\n * document's markers enormous and a long one's hairline, so the same rail reads as a different\n * control depending on what is in it. A fixed pitch means the rail thins to what fits and every\n * tick looks the same. @default 8\n */\n tickSize?: number;\n /** Width of the rail, in px, which is also how far a tick extends at the peak of the wave. @default 32 (2rem) */\n width?: number;\n onSelect?: (marker: OutlineMarker, index: number) => void;\n /**\n * Step one item, in whatever the host counts items in.\n *\n * Not one tick: the rail thins its markers to what fits, so the next tick can be ten items away,\n * and a reader pressing an arrow means \"the next one\" rather than \"the next thing I can see\". The\n * host steps it with the same mechanism its toolbar uses, which is why this is a callback rather\n * than something the rail decides.\n */\n onNavigate?: (delta: number) => void;\n}>;\n\n/**\n * A vertical rail of horizontal ticks, each representing an anchor marker in a\n * scrollable document. The rows tile the full height, so hovering anywhere in the rail activates\n * the nearest tick: it (and its neighbours, with a distance falloff) extends rightward in a wave.\n * A single popover — anchored to the tick the reader is at — shows the\n * marker's title/description. Markers whose range intersects `visibleRange` render at full\n * opacity; the rest are dimmed.\n */\n/** A rendered tick: the marker it points at, and the span of the document it stands for. */\ntype Row = { marker: OutlineMarker; index: number; span: { from: number; to: number } };\n\n/**\n * Evenly spreads `markers` over at most `capacity` rows, keeping the first and last.\n *\n * Each rendered tick then stands for the span up to the next one, so a thinned rail still covers\n * the whole document — a position between two ticks belongs to the one above it, rather than to\n * nothing.\n */\nconst thin = (markers: OutlineMarker[], capacity: number): Row[] => {\n const step = capacity > 1 ? (markers.length - 1) / (capacity - 1) : 0;\n const indexes =\n markers.length <= capacity\n ? markers.map((_, index) => index)\n : Array.from({ length: capacity }, (_, row) => Math.round(row * step));\n\n return indexes.map((index, row) => ({\n marker: markers[index],\n index,\n span: { from: markers[index].range.from, to: markers[indexes[row + 1]]?.range.from ?? markers[index].range.to },\n }));\n};\n\nexport const Outline = ({\n classNames,\n markers,\n visibleRange,\n tickSize = TICK_SIZE,\n width = DEFAULT_WIDTH,\n onSelect,\n onNavigate,\n}: OutlineProps) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const rowsRef = useRef<Array<HTMLButtonElement | null>>([]);\n // Two ways to be *at* a tick, kept apart: the pointer is over one, or the rail has focus and the\n // reader has navigated to one. Conflating them means leaving with the mouse cannot dismiss the\n // card — the keyboard position immediately puts it back.\n const [pointer, setPointer] = useState<number | null>(null);\n const [navigated, setNavigated] = useState<number | null>(null);\n // The rail is bounded by whatever contains it. Measured rather than assumed, because the same\n // component is used both in a sized grid cell and floating over a document, where nothing\n // constrains it and its natural height is the right one.\n const [available, setAvailable] = useState(0);\n useLayoutEffect(() => {\n const parent = containerRef.current?.parentElement;\n if (!parent) {\n return;\n }\n\n const observer = new ResizeObserver(([entry]) => setAvailable(entry.contentRect.height));\n observer.observe(parent);\n setAvailable(parent.getBoundingClientRect().height);\n return () => observer.disconnect();\n }, []);\n\n const natural = markers.length * tickSize;\n // Below two rows the measurement is a transient (an unlaid-out parent), not a real constraint.\n const bounded = available >= tickSize * 2;\n const rows = useMemo(\n () => thin(markers, bounded ? Math.min(markers.length, Math.floor(available / tickSize)) : markers.length),\n [markers, bounded, available, tickSize],\n );\n\n // Rest is full opacity when there is no visible range to compare against. Compared against the\n // row's span rather than its marker, so a thinned rail still lights the tick the reader is under.\n const isActive = useCallback(\n (row: Row) => (visibleRange ? intersects(row.span, visibleRange) : true),\n [visibleRange],\n );\n\n // Not \"has focus\" — \"is being driven by the keyboard\". A click focuses a tick too, and gating on\n // focus left the card up after the pointer had gone: the reader clicked, moved away, and the\n // keyboard position they never asked for kept it open.\n const [keyboard, setKeyboard] = useState(false);\n\n // Whichever the reader is actually using. The pointer is the more direct statement, so it wins by\n // default — but not once the arrows are in play: clicking a tick and then pressing an arrow leaves\n // the pointer resting where it was clicked, and a pointer that always wins pins the card to a tick\n // the reader has already navigated away from. Pointing again takes it back (`onPointerEnter`).\n const shown = keyboard ? (navigated ?? pointer) : (pointer ?? navigated);\n\n // A backstop while a card is up: the pointer arriving anywhere outside the rail clears it.\n //\n // The tick's own leave is enough for a plain hover, and is not enough after a click — the popover\n // that opens brings focus guards that sit over the page, and the tick never sees the pointer go.\n // Verified with a real pointer: hovering away cleared it, clicking and then hovering away did not.\n // Asking the document is the one question no overlay can intercept.\n useEffect(() => {\n if (pointer == null) {\n return;\n }\n\n const rail = containerRef.current;\n const owner = rail?.ownerDocument;\n if (!rail || !owner) {\n return;\n }\n\n const onOver = (event: PointerEvent) => {\n if (!rail.contains(event.target as Node | null)) {\n setPointer(null);\n }\n };\n\n owner.addEventListener('pointerover', onOver);\n return () => owner.removeEventListener('pointerover', onOver);\n }, [pointer]);\n\n // Offset of the shown tick, read from the tick itself: the rail centres its ticks, so the position\n // is not `index * tickSize` and asking the element is the only answer that stays right.\n const [anchorOffset, setAnchorOffset] = useState(0);\n useLayoutEffect(() => {\n const row = shown == null ? null : rowsRef.current[shown];\n if (row) {\n setAnchorOffset(row.offsetTop + row.offsetHeight / 2);\n }\n }, [shown]);\n\n // Wave falloff: a Gaussian (normal-distribution) bell centred on the tick shown — a rounded\n // peak with inflected shoulders that flatten toward rest, rather than a dome. `WAVE_SPREAD` is\n // the standard deviation (in rows).\n const widthFor = useCallback(\n (index: number) => {\n if (shown == null) {\n return REST_WIDTH;\n }\n\n const distance = index - shown;\n const falloff = Math.exp(-(distance * distance) / (2 * WAVE_SPREAD * WAVE_SPREAD));\n return REST_WIDTH + (width - REST_WIDTH) * falloff;\n },\n [shown, width],\n );\n\n // Arrow keys walk the rail. The ticks are buttons, so tab reaches the rail and the arrows then\n // move within it — the roving behaviour a vertical list of controls is expected to have. The step\n // while the keyboard is still on a tick; and clamped to `rows`, since thinning can leave the ref\n // array longer than the rail it now renders.\n const handleKeyDown = useCallback(\n (event: ReactKeyboardEvent<HTMLDivElement>) => {\n const delta = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;\n if (!delta) {\n return;\n }\n\n // Navigation wins where the host offers it: a focused tick is a place in the document, so the\n // arrows move through the document rather than through the rail's own ticks — which are\n // thinned, and so are not one-to-one with anything the reader is counting.\n if (onNavigate) {\n event.preventDefault();\n setKeyboard(true);\n onNavigate(delta);\n return;\n }\n\n // Otherwise they walk the rail, the roving behaviour a vertical list of controls is expected\n // to have. Taken from the focused element rather than from the shown tick, and clamped to\n // `rows`, since thinning can leave the ref array longer than the rail it now renders.\n const current = rowsRef.current.findIndex((element) => element === event.target);\n if (current < 0) {\n return;\n }\n\n const next = Math.min(Math.max(current + delta, 0), rows.length - 1);\n event.preventDefault();\n rowsRef.current[next]?.focus();\n },\n [rows.length, onNavigate],\n );\n\n // While the rail has focus, the shown tick follows where the reader has got to.\n //\n // Navigation steps items and the rail shows ticks, and the two are not one-to-one — so after an\n // arrow press the popover would otherwise describe the tick the pointer last touched rather than\n // the place the reader is now. Derived from the visible range, so it tracks the list however the\n // reader moved: the arrows, the toolbar, or the scrollbar.\n const visibleFrom = visibleRange?.from;\n useEffect(() => {\n if (!keyboard || visibleFrom == null) {\n setNavigated(null);\n return;\n }\n\n const index = rows.findIndex((row) => visibleFrom < row.span.to);\n setNavigated(index < 0 ? rows.length - 1 : index);\n }, [keyboard, visibleFrom, rows]);\n\n const hoveredMarker = shown == null ? undefined : rows[shown]?.marker;\n\n return (\n <Popover.Root\n open={hoveredMarker != null}\n // Pinned to the anchor point rather than flipped into view, so it tracks the tick.\n positioning={{ placement: 'right', flip: false, slide: false }}\n autoFocus={false}\n >\n <div\n role='navigation'\n className={mx('relative flex flex-col justify-center overflow-hidden', classNames)}\n style={{\n width: `${width}px`,\n height: bounded ? '100%' : `${natural}px`,\n }}\n // Published so a test can ask *why* a card is up — the pointer being over a tick, or the\n // keyboard having navigated to one — rather than only that it is (§12).\n data-pointer={pointer ?? ''}\n data-navigated={navigated ?? ''}\n // And which of the two is actually being shown, since that is the invariant — the reader\n // sees one card, not two states (§12).\n data-shown={shown ?? ''}\n // A pointer anywhere in the rail ends the keyboard's claim on what is shown: the reader has\n // gone back to pointing, and the two should not both assert a position.\n onPointerDown={() => setKeyboard(false)}\n onBlurCapture={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n setKeyboard(false);\n }\n }}\n onKeyDown={handleKeyDown}\n ref={containerRef}\n >\n {/* Ticks are a fixed height, so a document with fewer markers than the rail can hold does not\n fill it — and they are centred rather than stacked at the top, which reads as the rail\n having been cut short. Centring is the rail's own business; where the *rail* sits is its\n container's. */}\n {rows.map((row, index) => {\n const marker = row.marker;\n const active = isActive(row);\n const tick = (\n <button\n key={marker.id}\n type='button'\n aria-label={marker.title}\n // Full-width row; flexes to fill a bounded rail height (min height keeps it hoverable\n // when the rail is unbounded), so any point in the rail maps to a tick. The focus\n // ring is suppressed because focus already shows as the tick extending and\n // darkening — an outline around an invisible full-width row reads as a stray box.\n // `flex-1` with `min-h-0`: the rows share whatever height the rail has, so N markers\n // spread over the full range rather than stacking at their natural pitch and leaving\n // the rest blank. Thinning above guarantees they never need less than the pitch.\n className='flex items-center w-full shrink-0 cursor-pointer outline-none'\n style={{ height: tickSize }}\n ref={(element) => {\n rowsRef.current[index] = element;\n }}\n onPointerEnter={() => {\n setPointer(index);\n // The reader has gone back to pointing, so the keyboard's claim on what is shown\n // ends here rather than at a blur that a mouse never causes.\n setKeyboard(false);\n }}\n // Cleared on the tick that set it, rather than on the rail. A rail-level leave is\n // synthesised from over/out pairs and was observed not firing at all — the card\n // stayed up after the pointer had gone, in the browser and in a test. Moving between\n // ticks does not flicker: the leave and the next enter land in one batch.\n onPointerLeave={() => setPointer((current) => (current === index ? null : current))}\n onFocus={() => keyboard && setNavigated(index)}\n onClick={() => onSelect?.(marker, row.index)}\n >\n <div\n className={mx(\n 'h-[3px] rounded-full transition-all duration-200 ease-out',\n shown === index ? 'bg-neutral-800 dark:bg-neutral-200' : 'bg-neutral-400 dark:bg-neutral-600',\n active ? 'opacity-100' : 'opacity-30',\n )}\n style={{ width: widthFor(index) }}\n />\n </button>\n );\n\n return tick;\n })}\n\n {/* A separate anchor, and the ticks are never wrapped.\n Wrapping the shown tick in `Popover.Anchor` changes the element type at that position, so\n React unmounts and remounts that button — and a destroyed element never receives\n `pointerleave`, which is why the card would not dismiss when the pointer left. Verified\n with a real pointer: `data-pointer` stayed on the tick after the pointer had gone.\n A zero-height anchor moved to the shown tick's offset keeps every tick stable; the\n popover is keyed to the marker so it still re-measures when the anchor moves. */}\n <Popover.Anchor asChild>\n <div className='absolute left-0' style={{ top: anchorOffset, width, height: 0 }} />\n </Popover.Anchor>\n </div>\n {hoveredMarker && (\n <Popover.Content\n // Keyed to the tick: the popover measures its anchor when it mounts, and moving the anchor\n // to a different element does not make it measure again — the card stayed put while the\n // pointer walked the rail, drifting further from the tick with every step. Remounting per\n // tick is what makes it re-measure.\n key={hoveredMarker.id}\n // Wide enough for a prompt's first lines, but never past the space beside the rail.\n classNames='w-[32rem] max-w-(--available-width)'\n >\n {/* A plain column rather than `Popover.Body`: a hover card neither scrolls nor needs the body's gutter grid. */}\n <Layout.Flex column classNames='gap-1 px-2 py-1'>\n <p className='font-medium line-clamp-2'>{hoveredMarker.title}</p>\n {hoveredMarker.description && (\n <p className='text-sm text-fg-muted line-clamp-4'>{hoveredMarker.description}</p>\n )}\n </Layout.Flex>\n </Popover.Content>\n )}\n </Popover.Root>\n );\n};\n"],"mappings":"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"}