@athenaintel/react 0.50.0 → 0.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/agent-runtime/src/desktop-handoff.d.ts +10 -0
- package/dist/agent-runtime/src/edit-command.cjs +21 -0
- package/dist/agent-runtime/src/edit-command.cjs.map +1 -1
- package/dist/agent-runtime/src/edit-command.d.ts +8 -0
- package/dist/agent-runtime/src/edit-command.js +21 -0
- package/dist/agent-runtime/src/edit-command.js.map +1 -1
- package/dist/agent-runtime/src/handoff-arrival.cjs +19 -0
- package/dist/agent-runtime/src/handoff-arrival.cjs.map +1 -1
- package/dist/agent-runtime/src/handoff-arrival.d.ts +18 -3
- package/dist/agent-runtime/src/handoff-arrival.js +20 -1
- package/dist/agent-runtime/src/handoff-arrival.js.map +1 -1
- package/dist/agent-runtime/src/handoff-cursor-id.cjs +6 -0
- package/dist/agent-runtime/src/handoff-cursor-id.cjs.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor-id.d.ts +12 -0
- package/dist/agent-runtime/src/handoff-cursor-id.js +6 -0
- package/dist/agent-runtime/src/handoff-cursor-id.js.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor.cjs +88 -0
- package/dist/agent-runtime/src/handoff-cursor.cjs.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor.d.ts +120 -0
- package/dist/agent-runtime/src/handoff-cursor.js +88 -0
- package/dist/agent-runtime/src/handoff-cursor.js.map +1 -0
- package/dist/agent-runtime/src/index.d.ts +4 -3
- package/dist/agent-runtime/src/thread-handoff-flow-machine.d.ts +6 -0
- package/dist/agent-runtime/src/thread-handoff-protocol.d.ts +114 -4
- package/dist/agent-runtime/src/thread-handoff.d.ts +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +8 -3
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.d.ts +7 -0
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js +9 -4
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
- package/dist/agent-runtime/src/use-thread-handoff-flow.d.ts +5 -0
- package/dist/chat/AthenaChat.cjs +27 -12
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.d.ts +13 -0
- package/dist/chat/AthenaChat.js +27 -12
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/AthenaPlanStrip.cjs +97 -0
- package/dist/chat/AthenaPlanStrip.cjs.map +1 -0
- package/dist/chat/AthenaPlanStrip.d.ts +15 -0
- package/dist/chat/AthenaPlanStrip.js +97 -0
- package/dist/chat/AthenaPlanStrip.js.map +1 -0
- package/dist/chat/super-grouping/SuperGroupingCard.cjs +4 -2
- package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingCard.js +4 -2
- package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +4 -2
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.js +4 -2
- package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.cjs +6 -4
- package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.d.ts +9 -1
- package/dist/chat/super-grouping/activity-timeline.js +6 -4
- package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
- package/dist/chat/super-grouping/index.d.ts +1 -1
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.cjs +17 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.cjs.map +1 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.d.ts +8 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.js +17 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.js.map +1 -0
- package/dist/chat-ui/index.cjs +8 -0
- package/dist/chat-ui/index.cjs.map +1 -1
- package/dist/chat-ui/index.d.ts +2 -1
- package/dist/chat-ui/index.js +9 -1
- package/dist/chat-ui/index.js.map +1 -1
- package/dist/chat-ui/thread/chat-thread-shell.cjs +2 -0
- package/dist/chat-ui/thread/chat-thread-shell.cjs.map +1 -1
- package/dist/chat-ui/thread/chat-thread-shell.d.ts +4 -0
- package/dist/chat-ui/thread/chat-thread-shell.js +2 -0
- package/dist/chat-ui/thread/chat-thread-shell.js.map +1 -1
- package/dist/chat-ui/thread/handoff-cursor.cjs +216 -0
- package/dist/chat-ui/thread/handoff-cursor.cjs.map +1 -0
- package/dist/chat-ui/thread/handoff-cursor.d.ts +57 -0
- package/dist/chat-ui/thread/handoff-cursor.js +216 -0
- package/dist/chat-ui/thread/handoff-cursor.js.map +1 -0
- package/dist/chat-ui/thread/statewire-history-edge.cjs +2 -0
- package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.d.ts +2 -0
- package/dist/chat-ui/thread/statewire-history-edge.js +3 -1
- package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs +20 -3
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs.map +1 -1
- package/dist/keryx-http/src/unconfirmed-update-attributions.d.ts +11 -2
- package/dist/keryx-http/src/unconfirmed-update-attributions.js +21 -4
- package/dist/keryx-http/src/unconfirmed-update-attributions.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/tools/write-todos-tool-ui.cjs +8 -0
- package/dist/tools/write-todos-tool-ui.cjs.map +1 -1
- package/dist/tools/write-todos-tool-ui.d.ts +2 -0
- package/dist/tools/write-todos-tool-ui.js +8 -0
- package/dist/tools/write-todos-tool-ui.js.map +1 -1
- package/dist/workbench/session-workbench-panes.cjs +1 -8
- package/dist/workbench/session-workbench-panes.cjs.map +1 -1
- package/dist/workbench/session-workbench-panes.js +2 -9
- package/dist/workbench/session-workbench-panes.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff-cursor.js","sources":["../../../src/chat-ui/thread/handoff-cursor.tsx"],"sourcesContent":["/**\n * Handovers open where the person was reading (`@athenaintel/agent-runtime`'s\n * `handoff-cursor`), for every host of `ChatThreadShell` (Athena in Chrome,\n * the desktop app, `AthenaChat`).\n *\n * As a source, `useReadThreadReadingCursor` reads the shell's viewport at the\n * moment of the click: `null` at the bottom, else the id of the last message\n * fully in view. As a destination, `HandoffCursorReveal` (mounted by the shell\n * for its `arrivalThreadId`) scrolls to the message the arrival names once the\n * transcript has it, paging older history in when it is further back, and\n * highlights it briefly; a cursor it can't find leaves the view at the bottom.\n */\nimport {\n type ThreadMessage,\n type ThreadViewportState,\n useAui,\n useThreadViewport,\n useThreadViewportStore,\n} from \"@assistant-ui/react\";\nimport {\n deepAgentHistoryHasOlder,\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n HANDOFF_CURSOR_SETTLE_MS,\n handoffCursorMessageId,\n handoffCursorRevealStep,\n loadOlderDeepAgentHistoryPage,\n MESSAGE_ROW_ID_ATTRIBUTE,\n readDeepAgentStatewireExtras,\n readingCursorOf,\n settleHandoffArrivalCursor,\n useHandoffArrivalCursor,\n} from \"@athenaintel/agent-runtime\";\nimport type { FC } from \"react\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { THREAD_VIEWPORT_ATTR, THREAD_VIEWPORT_FOOTER_ATTR } from \"./statewire-history-edge\";\n/** What assistant-ui's `ThreadPrimitive.Root` stamps on the thread panel. */\nconst THREAD_ROOT_SELECTOR = \"[data-aui-thread-root]\";\n/** Set on the revealed message while it is highlighted. */\nexport const HANDOFF_CURSOR_REVEALED_ATTR = \"data-handoff-cursor-revealed\";\n/** How often a landing looks for its message while the transcript loads. */\nexport const HANDOFF_CURSOR_POLL_MS = 150;\n\nconst escapeAttributeValue = (value: string): string =>\n typeof CSS !== \"undefined\" && typeof CSS.escape === \"function\"\n ? CSS.escape(value)\n : value.replace(/[\"\\\\]/g, \"\\\\$&\");\n\n/** The thread viewport `anchor` sits in, or the one of the thread panel it sits in. */\nexport function findThreadViewport(anchor: Element | null | undefined): HTMLElement | null {\n if (!anchor) return null;\n const inside = anchor.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`);\n if (inside) return inside;\n return (\n anchor.closest(THREAD_ROOT_SELECTOR)?.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ??\n null\n );\n}\n\n/** How much of `viewport` its sticky footer (the composer dock) covers at the bottom. */\nfunction coveredBottomOf(viewport: HTMLElement): number {\n const footer = viewport.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_FOOTER_ATTR}]`);\n if (!footer) return 0;\n return Math.max(0, viewport.getBoundingClientRect().bottom - footer.getBoundingClientRect().top);\n}\n\n/** Where `viewport` is being read: `null` at the bottom, else a message id. */\nexport function readThreadReadingCursor(viewport: HTMLElement | null): string | null {\n if (!viewport) return null;\n return readingCursorOf({ viewport, coveredBottomPx: coveredBottomOf(viewport) });\n}\n\n/**\n * Reads where the thread is being read, at the moment of a click: inside the\n * viewport (a composer toolbar, even from a portaled dialog) through the\n * thread's viewport store; elsewhere in the panel (a header menu) from the\n * DOM around `anchor`. `null` at the bottom or with no viewport found.\n */\nexport function useReadThreadReadingCursor(): (anchor?: Element | null) => string | null {\n const store = useThreadViewportStore({ optional: true });\n return useCallback(\n (anchor?: Element | null) =>\n readThreadReadingCursor(store?.getState().element.viewport ?? findThreadViewport(anchor)),\n [store],\n );\n}\n\n/** Scrolls `element` into `viewport`'s visible part: centred, or its top when taller. */\nfunction scrollMessageIntoView({\n viewport,\n element,\n}: {\n viewport: HTMLElement;\n element: HTMLElement;\n}): void {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n const visible = Math.max(1, box.height - coveredBottomOf(viewport));\n const offsetTop = rect.top - box.top + viewport.scrollTop;\n const top =\n rect.height >= visible ? offsetTop - 8 : offsetTop - (visible - rect.height) / 2;\n // `instant`: the shell's viewport scrolls smoothly by default.\n viewport.scrollTo({ top: Math.max(0, top), behavior: \"instant\" });\n}\n\nfunction isInView({ viewport, element }: { viewport: HTMLElement; element: HTMLElement }): boolean {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n return rect.bottom > box.top && rect.top < box.bottom - coveredBottomOf(viewport);\n}\n\n/**\n * A brief tint on the revealed message, through the Web Animations API so it\n * needs nothing from the host's stylesheet. Returns its undo.\n */\nfunction highlightMessage(element: HTMLElement): () => void {\n element.setAttribute(HANDOFF_CURSOR_REVEALED_ATTR, \"\");\n const animation =\n typeof element.animate === \"function\"\n ? element.animate(\n [\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n offset: 0.5,\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0)\",\n borderRadius: \"12px\",\n },\n ],\n { duration: HANDOFF_CURSOR_HIGHLIGHT_MS, easing: \"ease-out\" },\n )\n : null;\n const timer = setTimeout(\n () => element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR),\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n );\n return () => {\n clearTimeout(timer);\n animation?.cancel();\n element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR);\n };\n}\n\ninterface ThreadSnapshot {\n messages: readonly ThreadMessage[];\n isLoading: boolean;\n extras: unknown;\n}\n\n/**\n * Opens the transcript at `cursor`: looks for the message every\n * {@link HANDOFF_CURSOR_POLL_MS}, pages older history in while it is further\n * back, scrolls to it, holds it in view while the transcript settles (until\n * the person scrolls), and highlights it. Calls `onSettled` once it revealed\n * the message or gave up (the bottom). Returns its cancel.\n */\nexport function startHandoffCursorReveal({\n cursor,\n readThread,\n readViewport,\n onSettled,\n now = () => Date.now(),\n}: {\n cursor: string;\n readThread: () => ThreadSnapshot;\n readViewport: () => HTMLElement | null;\n onSettled: () => void;\n now?: () => number;\n}): () => void {\n // Progress restarts the wait: the reveal started, or a page of older history landed.\n let waitingSince = now();\n let pagesLoaded = 0;\n let paging = false;\n // A page that landed renders on a later commit: look again before paging on.\n let pageLandsAt = 0;\n let settled = false;\n let revealed: { element: HTMLElement; viewport: HTMLElement; at: number } | null = null;\n let personScrolled = false;\n let unhighlight: (() => void) | null = null;\n let detach: (() => void) | null = null;\n\n const settle = () => {\n if (settled) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n onSettled();\n };\n const pageOlder = () => {\n paging = true;\n loadOlderDeepAgentHistoryPage({\n readExtras: () => readDeepAgentStatewireExtras(readThread().extras),\n })\n .then((loaded) => {\n if (!loaded) {\n // Nothing older could be stated: the history is exhausted.\n pagesLoaded = Number.POSITIVE_INFINITY;\n return;\n }\n pagesLoaded += 1;\n waitingSince = now();\n })\n .catch(() => {\n pagesLoaded = Number.POSITIVE_INFINITY;\n })\n .finally(() => {\n paging = false;\n pageLandsAt = now() + 2 * HANDOFF_CURSOR_POLL_MS;\n });\n };\n const tick = () => {\n if (settled) return;\n if (revealed) {\n if (personScrolled || now() - revealed.at >= HANDOFF_CURSOR_SETTLE_MS) {\n settle();\n return;\n }\n if (revealed.element.isConnected && !isInView(revealed)) scrollMessageIntoView(revealed);\n return;\n }\n const viewport = readViewport();\n const { messages, isLoading, extras } = readThread();\n const targetId = handoffCursorMessageId({ messages, cursor });\n const element =\n targetId !== null && viewport\n ? viewport.querySelector<HTMLElement>(\n `[${MESSAGE_ROW_ID_ATTRIBUTE}=\"${escapeAttributeValue(targetId)}\"]`,\n )\n : null;\n const step = handoffCursorRevealStep({\n found: element !== null,\n // Still attaching, a page in flight, or the row not rendered yet.\n loading:\n isLoading ||\n paging ||\n now() < pageLandsAt ||\n messages.length === 0 ||\n (targetId !== null && !element),\n hasOlder: deepAgentHistoryHasOlder(extras),\n pagesLoaded,\n elapsedMs: now() - waitingSince,\n });\n switch (step) {\n case \"reveal\": {\n if (!viewport || !element) return;\n scrollMessageIntoView({ viewport, element });\n unhighlight = highlightMessage(element);\n revealed = { element, viewport, at: now() };\n const stop = () => {\n personScrolled = true;\n };\n const events = [\"wheel\", \"touchstart\", \"pointerdown\", \"keydown\"] as const;\n for (const event of events) viewport.addEventListener(event, stop, { passive: true });\n detach = () => {\n for (const event of events) viewport.removeEventListener(event, stop);\n };\n return;\n }\n case \"page\":\n pageOlder();\n return;\n case \"wait\":\n return;\n case \"bottom\":\n settle();\n return;\n }\n };\n const timer = setInterval(tick, HANDOFF_CURSOR_POLL_MS);\n tick();\n return () => {\n // Once settled, the highlight fades out on its own timer.\n if (settled) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n unhighlight?.();\n };\n}\n\n/**\n * Opens `threadId`'s transcript at the message the source was reading, once,\n * when this surface picked the conversation up with a cursor\n * (`recordHandoffArrival({ cursor })`). Renders nothing.\n */\nexport const HandoffCursorReveal: FC<{ threadId: string | null | undefined }> = ({ threadId }) => {\n const cursor = useHandoffArrivalCursor(threadId);\n const aui = useAui();\n const viewport = useThreadViewport({\n optional: true,\n selector: (state: ThreadViewportState) => state.element.viewport,\n });\n const viewportRef = useRef(viewport);\n viewportRef.current = viewport;\n useEffect(() => {\n if (!threadId || !cursor) return undefined;\n return startHandoffCursorReveal({\n cursor,\n readThread: () => {\n const state = aui.thread().getState();\n return { messages: state.messages, isLoading: state.isLoading, extras: state.extras };\n },\n readViewport: () => viewportRef.current ?? null,\n onSettled: () => settleHandoffArrivalCursor(threadId),\n });\n }, [aui, cursor, threadId]);\n return null;\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -10,6 +10,7 @@ const useDeepAgentWindowSync = require("../../agent-runtime/src/use-deep-agent-w
|
|
|
10
10
|
const errors = require("../../agent-runtime/src/errors.cjs");
|
|
11
11
|
const cnfast = require("cnfast");
|
|
12
12
|
const THREAD_VIEWPORT_ATTR = "data-aui-thread-viewport";
|
|
13
|
+
const THREAD_VIEWPORT_FOOTER_ATTR = "data-aui-thread-viewport-footer";
|
|
13
14
|
const HISTORY_EDGE_NEAR_MARGIN_PX = 1500;
|
|
14
15
|
const HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY = 2;
|
|
15
16
|
const MESSAGE_SELECTOR = "[data-message-id]";
|
|
@@ -251,4 +252,5 @@ exports.HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY = HISTORY_EDGE_TOOL_OUTPUT_CONCURRE
|
|
|
251
252
|
exports.HistoryEdgeRow = HistoryEdgeRow;
|
|
252
253
|
exports.StatewireHistoryEdge = StatewireHistoryEdge;
|
|
253
254
|
exports.THREAD_VIEWPORT_ATTR = THREAD_VIEWPORT_ATTR;
|
|
255
|
+
exports.THREAD_VIEWPORT_FOOTER_ATTR = THREAD_VIEWPORT_FOOTER_ATTR;
|
|
254
256
|
//# sourceMappingURL=statewire-history-edge.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statewire-history-edge.cjs","sources":["../../../src/chat-ui/thread/statewire-history-edge.tsx"],"sourcesContent":["import {\n getExternalStoreMessages,\n type ThreadMessage,\n type ThreadViewportState,\n useAuiState,\n useThreadViewport,\n useThreadViewportStore,\n} from \"@assistant-ui/react\";\nimport {\n type DeepAgentHistoryLoadEvent,\n type DeepAgentToolOutputStore,\n isStatewireChannelTeardown,\n useDeepAgentWindowSync,\n} from \"@athenaintel/agent-runtime\";\nimport type { FC } from \"react\";\nimport { Component, useContext, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n NO_STATEWIRE_HISTORY,\n StatewireHistoryContext,\n} from \"../../provider/statewire-history-context\";\nimport { useStatewireToolOutputs } from \"../../provider/statewire-window-context\";\nimport { AthenaStatewireThreadIdContext } from \"../../threads/thread-id-context\";\n\n/** The attribute `ChatThreadShell` stamps on the scroll viewport. */\nexport const THREAD_VIEWPORT_ATTR = \"data-aui-thread-viewport\";\n\n/**\n * How far above the viewport the edge row counts as near: the row sits above\n * the first loaded message, so reaching it means the user has scrolled past\n * everything loaded and the next page should already be on its way. Messages\n * this close to the viewport also count as on screen for cut tool outputs.\n */\nexport const HISTORY_EDGE_NEAR_MARGIN_PX = 1500;\n\n/**\n * Cut tool outputs a transcript reads at once. The reads share the session's\n * command lane with everything the person sends, so a long transcript must\n * not queue dozens of them ahead of a message or a Stop.\n */\nexport const HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY = 2;\n\n/** What assistant-ui's `MessagePrimitive.Root` stamps on every message. */\nconst MESSAGE_SELECTOR = \"[data-message-id]\";\n\nconst NO_MESSAGE_IDS: ReadonlySet<string> = new Set();\n\nfunction sameIds({ a, b }: { a: ReadonlySet<string>; b: ReadonlySet<string> }): boolean {\n if (a.size !== b.size) return false;\n for (const id of a) if (!b.has(id)) return false;\n return true;\n}\n\n/**\n * Which messages sit within {@link HISTORY_EDGE_NEAR_MARGIN_PX} of the\n * viewport, so cut tool outputs are read for what the person can reach. A\n * thread opens at its newest turn; reading every loaded step's output up\n * front sent dozens of reads, each re-rendering the transcript, for steps\n * nobody had scrolled to.\n *\n * `undefined` (every message counts) where nothing can be observed: no\n * viewport around the row, or no `IntersectionObserver`. A message rendered\n * without a `data-message-id` root (a host's own renderer) cannot be observed\n * either, so it counts as before.\n */\nfunction useMessagesNearViewport({\n viewport,\n hasViewport,\n messages,\n}: {\n viewport: HTMLElement | null;\n /** A viewport is (or will be) registered around the row. */\n hasViewport: boolean;\n /** The converted thread: every change may have mounted or remounted a message. */\n messages: readonly ThreadMessage[];\n}): ((messageId: string) => boolean) | undefined {\n const [nearIds, setNearIds] = useState<ReadonlySet<string>>(NO_MESSAGE_IDS);\n const observable = hasViewport && typeof IntersectionObserver !== \"undefined\";\n const trackRef = useRef<(() => void) | null>(null);\n // Every id a rendered element carries, as of the last scan.\n const renderedIdsRef = useRef<ReadonlySet<string>>(NO_MESSAGE_IDS);\n\n useEffect(() => {\n if (!viewport || !observable) return undefined;\n const near = new Set<Element>();\n const observed = new Set<Element>();\n const publish = () => {\n const ids = new Set<string>();\n for (const element of near) {\n const id = element.getAttribute(\"data-message-id\");\n if (id) ids.add(id);\n }\n setNearIds((previous) => (sameIds({ a: previous, b: ids }) ? previous : ids));\n };\n const intersections = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting && entry.target.isConnected) near.add(entry.target);\n else near.delete(entry.target);\n }\n publish();\n },\n { root: viewport, rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px` },\n );\n // A page that folds into a turn gives it a new id: a host keyed by id\n // remounts the turn's element, a host keyed by row (`ChatThreadShell`)\n // keeps it and the element's id changes. So what is observed follows the\n // rendered elements, not the message list, and the near ids are read off\n // those elements again on every scan.\n const track = () => {\n for (const element of observed) {\n if (element.isConnected) continue;\n intersections.unobserve(element);\n observed.delete(element);\n near.delete(element);\n }\n const rendered = new Set<string>();\n for (const element of viewport.querySelectorAll(MESSAGE_SELECTOR)) {\n const id = element.getAttribute(\"data-message-id\");\n if (id) rendered.add(id);\n if (observed.has(element)) continue;\n observed.add(element);\n intersections.observe(element);\n }\n renderedIdsRef.current = rendered;\n publish();\n };\n trackRef.current = track;\n track();\n return () => {\n trackRef.current = null;\n renderedIdsRef.current = NO_MESSAGE_IDS;\n intersections.disconnect();\n setNearIds(NO_MESSAGE_IDS);\n };\n }, [viewport, observable]);\n\n // A changed message list is the trigger to re-scan: its messages render in\n // the same commit, so their elements are in the DOM by this effect (a\n // query, no layout read).\n useEffect(() => {\n trackRef.current?.();\n }, [messages]);\n\n return useMemo(() => {\n if (!observable) return undefined;\n return (messageId: string) => {\n // Not registered yet: nothing is on screen to read for.\n if (viewport === null) return false;\n return nearIds.has(messageId) || !renderedIdsRef.current.has(messageId);\n };\n }, [nearIds, observable, viewport]);\n}\n\n/** The wire message a converted message was built from first (its oldest source). */\nfunction oldestSourceId(message: ThreadMessage): string | undefined {\n const source = getExternalStoreMessages<{ id?: unknown }>(message)[0];\n return typeof source?.id === \"string\" ? source.id : undefined;\n}\n\n/**\n * Whether older history landed ahead of what was loaded: the old first\n * message now sits further down (a page ahead of it), or the first message's\n * old oldest wire message now follows earlier ones (a page that folded into\n * the first loaded turn). A first message replaced in place — a branch picked\n * on it, an edit — is neither, and a run streaming at the tail never touches\n * the first message.\n */\nfunction prependedOlderHistory({\n previous,\n next,\n}: {\n previous: readonly ThreadMessage[];\n next: readonly ThreadMessage[];\n}): boolean {\n const previousFirst = previous[0];\n const nextFirst = next[0];\n if (previousFirst === undefined || nextFirst === undefined || previousFirst === nextFirst) return false;\n if (previousFirst.id !== nextFirst.id && next.some((message, index) => index > 0 && message.id === previousFirst.id)) {\n return true;\n }\n const previousOldest = oldestSourceId(previousFirst);\n if (previousOldest === undefined || previousOldest === oldestSourceId(nextFirst)) return false;\n return getExternalStoreMessages<{ id?: unknown }>(nextFirst).some(\n (source, index) => index > 0 && source?.id === previousOldest,\n );\n}\n\ninterface KeepReadingPlaceProps {\n viewport: HTMLElement | null;\n threadId: string | null;\n /** The converted thread. */\n messages: readonly ThreadMessage[];\n}\n\n/**\n * Keeps the reader's place when older history lands. Everything a page adds\n * sits above what the person is reading, so their distance from the bottom of\n * the transcript is what must survive. A class for `getSnapshotBeforeUpdate`:\n * the distance is read right before React applies the commit that renders the\n * page — the one moment it is exact, even for a scroll the browser has not\n * reported yet — and restored after it, instantly (never through the\n * viewport's smooth scrolling). Other commits read no layout.\n *\n * The browser's scroll anchoring and assistant-ui's prepend anchor aim at the\n * same position, so whichever ran first leaves this nothing to do; on its own\n * it covers what they miss: a reader at the very top (browsers never anchor\n * at scroll offset 0) and a page that folds into the first loaded turn of a\n * host that keys rows by message id (that message remounts under a new id,\n * taking the browser's anchor node with it; `ChatThreadShell` keeps the row).\n */\nclass KeepReadingPlace extends Component<KeepReadingPlaceProps> {\n getSnapshotBeforeUpdate(previous: Readonly<KeepReadingPlaceProps>): number | null {\n const { viewport, threadId, messages } = this.props;\n if (\n viewport === null ||\n viewport !== previous.viewport ||\n threadId !== previous.threadId ||\n messages === previous.messages ||\n !prependedOlderHistory({ previous: previous.messages, next: messages })\n ) {\n return null;\n }\n return viewport.scrollHeight - viewport.scrollTop;\n }\n\n componentDidUpdate(\n _previous: Readonly<KeepReadingPlaceProps>,\n _state: unknown,\n fromBottom: number | null,\n ): void {\n const { viewport } = this.props;\n if (viewport === null || fromBottom === null) return;\n const top = viewport.scrollHeight - fromBottom;\n if (Math.abs(viewport.scrollTop - top) >= 1) viewport.scrollTo({ top, behavior: \"instant\" });\n }\n\n render(): null {\n return null;\n }\n}\n\nexport interface HistoryEdgeRowProps {\n /** Whether the server holds messages older than the loaded window. */\n hasOlder: boolean;\n /** Whether the next page can be asked for; `false` with `hasOlder` is the connection's loading depth spent. Defaults to `hasOlder`. */\n canLoadOlder?: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n onLoadOlder: () => void;\n className?: string;\n /** The row element, for the viewport observer. */\n rowRef?: (element: HTMLElement | null) => void;\n}\n\n/**\n * The top edge of the LOADED history: a manual way to load the next page of\n * older messages, and the in-flight state. Renders nothing when the whole\n * transcript is loaded (or the head interest has not resolved).\n */\nexport const HistoryEdgeRow: FC<HistoryEdgeRowProps> = ({\n hasOlder,\n canLoadOlder = hasOlder,\n loading,\n onLoadOlder,\n className,\n rowRef,\n}) => {\n if (!hasOlder) return null;\n return (\n <div\n ref={rowRef}\n data-testid=\"thread-history-edge\"\n className={cn(\n \"mx-auto flex w-full max-w-(--thread-max-width) items-center justify-center py-1 text-xs text-muted-foreground\",\n className,\n )}\n >\n {loading ? (\n <span data-testid=\"thread-history-edge-loading\">Loading earlier messages…</span>\n ) : canLoadOlder ? (\n <button\n type=\"button\"\n data-testid=\"thread-history-edge-load\"\n onClick={onLoadOlder}\n className=\"rounded-lg px-2.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground\"\n >\n Load earlier messages\n </button>\n ) : (\n <span data-testid=\"thread-history-edge-limit\">Earlier messages can't be loaded here</span>\n )}\n </div>\n );\n};\n\nexport interface StatewireHistoryEdgeProps {\n /**\n * The thread's tool-output store (the one its converter substitutes from).\n * Defaults to `AthenaProvider`'s; hosts mounting the runtime themselves pass\n * the store they gave `useDeepAgentRuntime({ toolOutputs })`.\n */\n toolOutputs?: DeepAgentToolOutputStore;\n className?: string;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: (event: DeepAgentHistoryLoadEvent) => void;\n}\n\n/**\n * The client half of the interest window for a DOM thread — mounted once per\n * thread, inside the scroll viewport above the messages (`ChatThreadShell`'s\n * `beforeMessages` slot):\n *\n * - renders the \"Load earlier messages\" edge row and loads the next page when\n * the row comes near the viewport (scrolling up) or on its button, keeping\n * the reading position when the page lands above the view;\n * - fetches cut tool outputs whole for the cards near the viewport, a few at\n * a time, so they re-render complete through the converter (`toolOutputs`).\n *\n * Inert until the head interest resolves: no row, no reads.\n */\nexport const StatewireHistoryEdge: FC<StatewireHistoryEdgeProps> = ({\n toolOutputs: toolOutputsProp,\n className,\n onHistoryLoad,\n}) => {\n const providerToolOutputs = useStatewireToolOutputs();\n const toolOutputs = toolOutputsProp ?? providerToolOutputs ?? undefined;\n const messages = useAuiState((s) => s.thread.messages);\n const threadId = useContext(AthenaStatewireThreadIdContext);\n const [nearTop, setNearTop] = useState(false);\n const [nearTopEpoch, setNearTopEpoch] = useState(0);\n const nearTopRef = useRef(false);\n const [rowEl, setRowEl] = useState<HTMLElement | null>(null);\n // The scroll viewport the thread registered (assistant-ui's viewport store),\n // known even while the row renders nothing; the row's ancestor otherwise.\n const hasViewportStore = useThreadViewportStore({ optional: true }) !== null;\n const registeredViewport = useThreadViewport({\n optional: true,\n selector: (state: ThreadViewportState) => state.element.viewport,\n });\n const viewport =\n registeredViewport ?? rowEl?.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ?? null;\n const isMessageMounted = useMessagesNearViewport({\n viewport,\n hasViewport: hasViewportStore || viewport !== null,\n messages,\n });\n const historyStore = useContext(StatewireHistoryContext);\n const { hasOlder, canLoadOlder, loading, loadOlder } = useDeepAgentWindowSync({\n messages,\n toolOutputs,\n nearTop,\n nearTopEpoch,\n isMessageMounted,\n maxConcurrentToolOutputs: HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY,\n onHistoryLoad,\n onError: (error, command) => {\n // Leaving the thread settles an in-flight read as lost: lifecycle, not a failure.\n if (error instanceof Error && isStatewireChannelTeardown(error)) return;\n console.warn(`[StatewireHistoryEdge] ${command} failed:`, error);\n },\n });\n\n // Other surfaces of the provider (the workbench's Shell pane) read where the\n // history stands; leaving the thread or unmounting withdraws the report.\n useEffect(() => {\n if (!historyStore || !threadId) return undefined;\n historyStore.set({ threadId, hasOlder, canLoadOlder, loading, loadOlder });\n return undefined;\n }, [historyStore, threadId, hasOlder, canLoadOlder, loading, loadOlder]);\n useEffect(() => {\n if (!historyStore || !threadId) return undefined;\n return () => {\n if (historyStore.get().threadId === threadId) historyStore.set(NO_STATEWIRE_HISTORY);\n };\n }, [historyStore, threadId]);\n\n // The row's own visibility is the near-top level: a level, not an edge, so\n // pages chain while the user stays at the top and stop once the prepended\n // content has pushed the row away again.\n useEffect(() => {\n if (!rowEl || !hasOlder || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n const near = entry.isIntersecting;\n if (nearTopRef.current === near) continue;\n nearTopRef.current = near;\n setNearTop(near);\n if (near) setNearTopEpoch((epoch) => epoch + 1);\n }\n },\n {\n root: viewport,\n rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px`,\n },\n );\n observer.observe(rowEl);\n return () => {\n observer.disconnect();\n nearTopRef.current = false;\n setNearTop(false);\n };\n }, [rowEl, hasOlder, viewport]);\n\n return (\n <>\n <KeepReadingPlace viewport={viewport} threadId={threadId} messages={messages} />\n <HistoryEdgeRow\n rowRef={setRowEl}\n hasOlder={hasOlder}\n canLoadOlder={canLoadOlder}\n loading={loading}\n onLoadOlder={loadOlder}\n className={className}\n />\n </>\n );\n};\n"],"names":["useState","useRef","useEffect","useMemo","getExternalStoreMessages","Component","jsx","cn","useStatewireToolOutputs","useAuiState","useContext","AthenaStatewireThreadIdContext","useThreadViewportStore","useThreadViewport","StatewireHistoryContext","useDeepAgentWindowSync","isStatewireChannelTeardown","NO_STATEWIRE_HISTORY","jsxs","Fragment"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"statewire-history-edge.cjs","sources":["../../../src/chat-ui/thread/statewire-history-edge.tsx"],"sourcesContent":["import {\n getExternalStoreMessages,\n type ThreadMessage,\n type ThreadViewportState,\n useAuiState,\n useThreadViewport,\n useThreadViewportStore,\n} from \"@assistant-ui/react\";\nimport {\n type DeepAgentHistoryLoadEvent,\n type DeepAgentToolOutputStore,\n isStatewireChannelTeardown,\n useDeepAgentWindowSync,\n} from \"@athenaintel/agent-runtime\";\nimport type { FC } from \"react\";\nimport { Component, useContext, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n NO_STATEWIRE_HISTORY,\n StatewireHistoryContext,\n} from \"../../provider/statewire-history-context\";\nimport { useStatewireToolOutputs } from \"../../provider/statewire-window-context\";\nimport { AthenaStatewireThreadIdContext } from \"../../threads/thread-id-context\";\n\n/** The attribute `ChatThreadShell` stamps on the scroll viewport. */\nexport const THREAD_VIEWPORT_ATTR = \"data-aui-thread-viewport\";\n/** The attribute `ChatThreadShell` stamps on its sticky viewport footer (the composer dock). */\nexport const THREAD_VIEWPORT_FOOTER_ATTR = \"data-aui-thread-viewport-footer\";\n\n/**\n * How far above the viewport the edge row counts as near: the row sits above\n * the first loaded message, so reaching it means the user has scrolled past\n * everything loaded and the next page should already be on its way. Messages\n * this close to the viewport also count as on screen for cut tool outputs.\n */\nexport const HISTORY_EDGE_NEAR_MARGIN_PX = 1500;\n\n/**\n * Cut tool outputs a transcript reads at once. The reads share the session's\n * command lane with everything the person sends, so a long transcript must\n * not queue dozens of them ahead of a message or a Stop.\n */\nexport const HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY = 2;\n\n/** What assistant-ui's `MessagePrimitive.Root` stamps on every message. */\nconst MESSAGE_SELECTOR = \"[data-message-id]\";\n\nconst NO_MESSAGE_IDS: ReadonlySet<string> = new Set();\n\nfunction sameIds({ a, b }: { a: ReadonlySet<string>; b: ReadonlySet<string> }): boolean {\n if (a.size !== b.size) return false;\n for (const id of a) if (!b.has(id)) return false;\n return true;\n}\n\n/**\n * Which messages sit within {@link HISTORY_EDGE_NEAR_MARGIN_PX} of the\n * viewport, so cut tool outputs are read for what the person can reach. A\n * thread opens at its newest turn; reading every loaded step's output up\n * front sent dozens of reads, each re-rendering the transcript, for steps\n * nobody had scrolled to.\n *\n * `undefined` (every message counts) where nothing can be observed: no\n * viewport around the row, or no `IntersectionObserver`. A message rendered\n * without a `data-message-id` root (a host's own renderer) cannot be observed\n * either, so it counts as before.\n */\nfunction useMessagesNearViewport({\n viewport,\n hasViewport,\n messages,\n}: {\n viewport: HTMLElement | null;\n /** A viewport is (or will be) registered around the row. */\n hasViewport: boolean;\n /** The converted thread: every change may have mounted or remounted a message. */\n messages: readonly ThreadMessage[];\n}): ((messageId: string) => boolean) | undefined {\n const [nearIds, setNearIds] = useState<ReadonlySet<string>>(NO_MESSAGE_IDS);\n const observable = hasViewport && typeof IntersectionObserver !== \"undefined\";\n const trackRef = useRef<(() => void) | null>(null);\n // Every id a rendered element carries, as of the last scan.\n const renderedIdsRef = useRef<ReadonlySet<string>>(NO_MESSAGE_IDS);\n\n useEffect(() => {\n if (!viewport || !observable) return undefined;\n const near = new Set<Element>();\n const observed = new Set<Element>();\n const publish = () => {\n const ids = new Set<string>();\n for (const element of near) {\n const id = element.getAttribute(\"data-message-id\");\n if (id) ids.add(id);\n }\n setNearIds((previous) => (sameIds({ a: previous, b: ids }) ? previous : ids));\n };\n const intersections = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting && entry.target.isConnected) near.add(entry.target);\n else near.delete(entry.target);\n }\n publish();\n },\n { root: viewport, rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px` },\n );\n // A page that folds into a turn gives it a new id: a host keyed by id\n // remounts the turn's element, a host keyed by row (`ChatThreadShell`)\n // keeps it and the element's id changes. So what is observed follows the\n // rendered elements, not the message list, and the near ids are read off\n // those elements again on every scan.\n const track = () => {\n for (const element of observed) {\n if (element.isConnected) continue;\n intersections.unobserve(element);\n observed.delete(element);\n near.delete(element);\n }\n const rendered = new Set<string>();\n for (const element of viewport.querySelectorAll(MESSAGE_SELECTOR)) {\n const id = element.getAttribute(\"data-message-id\");\n if (id) rendered.add(id);\n if (observed.has(element)) continue;\n observed.add(element);\n intersections.observe(element);\n }\n renderedIdsRef.current = rendered;\n publish();\n };\n trackRef.current = track;\n track();\n return () => {\n trackRef.current = null;\n renderedIdsRef.current = NO_MESSAGE_IDS;\n intersections.disconnect();\n setNearIds(NO_MESSAGE_IDS);\n };\n }, [viewport, observable]);\n\n // A changed message list is the trigger to re-scan: its messages render in\n // the same commit, so their elements are in the DOM by this effect (a\n // query, no layout read).\n useEffect(() => {\n trackRef.current?.();\n }, [messages]);\n\n return useMemo(() => {\n if (!observable) return undefined;\n return (messageId: string) => {\n // Not registered yet: nothing is on screen to read for.\n if (viewport === null) return false;\n return nearIds.has(messageId) || !renderedIdsRef.current.has(messageId);\n };\n }, [nearIds, observable, viewport]);\n}\n\n/** The wire message a converted message was built from first (its oldest source). */\nfunction oldestSourceId(message: ThreadMessage): string | undefined {\n const source = getExternalStoreMessages<{ id?: unknown }>(message)[0];\n return typeof source?.id === \"string\" ? source.id : undefined;\n}\n\n/**\n * Whether older history landed ahead of what was loaded: the old first\n * message now sits further down (a page ahead of it), or the first message's\n * old oldest wire message now follows earlier ones (a page that folded into\n * the first loaded turn). A first message replaced in place — a branch picked\n * on it, an edit — is neither, and a run streaming at the tail never touches\n * the first message.\n */\nfunction prependedOlderHistory({\n previous,\n next,\n}: {\n previous: readonly ThreadMessage[];\n next: readonly ThreadMessage[];\n}): boolean {\n const previousFirst = previous[0];\n const nextFirst = next[0];\n if (previousFirst === undefined || nextFirst === undefined || previousFirst === nextFirst) return false;\n if (previousFirst.id !== nextFirst.id && next.some((message, index) => index > 0 && message.id === previousFirst.id)) {\n return true;\n }\n const previousOldest = oldestSourceId(previousFirst);\n if (previousOldest === undefined || previousOldest === oldestSourceId(nextFirst)) return false;\n return getExternalStoreMessages<{ id?: unknown }>(nextFirst).some(\n (source, index) => index > 0 && source?.id === previousOldest,\n );\n}\n\ninterface KeepReadingPlaceProps {\n viewport: HTMLElement | null;\n threadId: string | null;\n /** The converted thread. */\n messages: readonly ThreadMessage[];\n}\n\n/**\n * Keeps the reader's place when older history lands. Everything a page adds\n * sits above what the person is reading, so their distance from the bottom of\n * the transcript is what must survive. A class for `getSnapshotBeforeUpdate`:\n * the distance is read right before React applies the commit that renders the\n * page — the one moment it is exact, even for a scroll the browser has not\n * reported yet — and restored after it, instantly (never through the\n * viewport's smooth scrolling). Other commits read no layout.\n *\n * The browser's scroll anchoring and assistant-ui's prepend anchor aim at the\n * same position, so whichever ran first leaves this nothing to do; on its own\n * it covers what they miss: a reader at the very top (browsers never anchor\n * at scroll offset 0) and a page that folds into the first loaded turn of a\n * host that keys rows by message id (that message remounts under a new id,\n * taking the browser's anchor node with it; `ChatThreadShell` keeps the row).\n */\nclass KeepReadingPlace extends Component<KeepReadingPlaceProps> {\n getSnapshotBeforeUpdate(previous: Readonly<KeepReadingPlaceProps>): number | null {\n const { viewport, threadId, messages } = this.props;\n if (\n viewport === null ||\n viewport !== previous.viewport ||\n threadId !== previous.threadId ||\n messages === previous.messages ||\n !prependedOlderHistory({ previous: previous.messages, next: messages })\n ) {\n return null;\n }\n return viewport.scrollHeight - viewport.scrollTop;\n }\n\n componentDidUpdate(\n _previous: Readonly<KeepReadingPlaceProps>,\n _state: unknown,\n fromBottom: number | null,\n ): void {\n const { viewport } = this.props;\n if (viewport === null || fromBottom === null) return;\n const top = viewport.scrollHeight - fromBottom;\n if (Math.abs(viewport.scrollTop - top) >= 1) viewport.scrollTo({ top, behavior: \"instant\" });\n }\n\n render(): null {\n return null;\n }\n}\n\nexport interface HistoryEdgeRowProps {\n /** Whether the server holds messages older than the loaded window. */\n hasOlder: boolean;\n /** Whether the next page can be asked for; `false` with `hasOlder` is the connection's loading depth spent. Defaults to `hasOlder`. */\n canLoadOlder?: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n onLoadOlder: () => void;\n className?: string;\n /** The row element, for the viewport observer. */\n rowRef?: (element: HTMLElement | null) => void;\n}\n\n/**\n * The top edge of the LOADED history: a manual way to load the next page of\n * older messages, and the in-flight state. Renders nothing when the whole\n * transcript is loaded (or the head interest has not resolved).\n */\nexport const HistoryEdgeRow: FC<HistoryEdgeRowProps> = ({\n hasOlder,\n canLoadOlder = hasOlder,\n loading,\n onLoadOlder,\n className,\n rowRef,\n}) => {\n if (!hasOlder) return null;\n return (\n <div\n ref={rowRef}\n data-testid=\"thread-history-edge\"\n className={cn(\n \"mx-auto flex w-full max-w-(--thread-max-width) items-center justify-center py-1 text-xs text-muted-foreground\",\n className,\n )}\n >\n {loading ? (\n <span data-testid=\"thread-history-edge-loading\">Loading earlier messages…</span>\n ) : canLoadOlder ? (\n <button\n type=\"button\"\n data-testid=\"thread-history-edge-load\"\n onClick={onLoadOlder}\n className=\"rounded-lg px-2.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground\"\n >\n Load earlier messages\n </button>\n ) : (\n <span data-testid=\"thread-history-edge-limit\">Earlier messages can't be loaded here</span>\n )}\n </div>\n );\n};\n\nexport interface StatewireHistoryEdgeProps {\n /**\n * The thread's tool-output store (the one its converter substitutes from).\n * Defaults to `AthenaProvider`'s; hosts mounting the runtime themselves pass\n * the store they gave `useDeepAgentRuntime({ toolOutputs })`.\n */\n toolOutputs?: DeepAgentToolOutputStore;\n className?: string;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: (event: DeepAgentHistoryLoadEvent) => void;\n}\n\n/**\n * The client half of the interest window for a DOM thread — mounted once per\n * thread, inside the scroll viewport above the messages (`ChatThreadShell`'s\n * `beforeMessages` slot):\n *\n * - renders the \"Load earlier messages\" edge row and loads the next page when\n * the row comes near the viewport (scrolling up) or on its button, keeping\n * the reading position when the page lands above the view;\n * - fetches cut tool outputs whole for the cards near the viewport, a few at\n * a time, so they re-render complete through the converter (`toolOutputs`).\n *\n * Inert until the head interest resolves: no row, no reads.\n */\nexport const StatewireHistoryEdge: FC<StatewireHistoryEdgeProps> = ({\n toolOutputs: toolOutputsProp,\n className,\n onHistoryLoad,\n}) => {\n const providerToolOutputs = useStatewireToolOutputs();\n const toolOutputs = toolOutputsProp ?? providerToolOutputs ?? undefined;\n const messages = useAuiState((s) => s.thread.messages);\n const threadId = useContext(AthenaStatewireThreadIdContext);\n const [nearTop, setNearTop] = useState(false);\n const [nearTopEpoch, setNearTopEpoch] = useState(0);\n const nearTopRef = useRef(false);\n const [rowEl, setRowEl] = useState<HTMLElement | null>(null);\n // The scroll viewport the thread registered (assistant-ui's viewport store),\n // known even while the row renders nothing; the row's ancestor otherwise.\n const hasViewportStore = useThreadViewportStore({ optional: true }) !== null;\n const registeredViewport = useThreadViewport({\n optional: true,\n selector: (state: ThreadViewportState) => state.element.viewport,\n });\n const viewport =\n registeredViewport ?? rowEl?.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ?? null;\n const isMessageMounted = useMessagesNearViewport({\n viewport,\n hasViewport: hasViewportStore || viewport !== null,\n messages,\n });\n const historyStore = useContext(StatewireHistoryContext);\n const { hasOlder, canLoadOlder, loading, loadOlder } = useDeepAgentWindowSync({\n messages,\n toolOutputs,\n nearTop,\n nearTopEpoch,\n isMessageMounted,\n maxConcurrentToolOutputs: HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY,\n onHistoryLoad,\n onError: (error, command) => {\n // Leaving the thread settles an in-flight read as lost: lifecycle, not a failure.\n if (error instanceof Error && isStatewireChannelTeardown(error)) return;\n console.warn(`[StatewireHistoryEdge] ${command} failed:`, error);\n },\n });\n\n // Other surfaces of the provider (the workbench's Shell pane) read where the\n // history stands; leaving the thread or unmounting withdraws the report.\n useEffect(() => {\n if (!historyStore || !threadId) return undefined;\n historyStore.set({ threadId, hasOlder, canLoadOlder, loading, loadOlder });\n return undefined;\n }, [historyStore, threadId, hasOlder, canLoadOlder, loading, loadOlder]);\n useEffect(() => {\n if (!historyStore || !threadId) return undefined;\n return () => {\n if (historyStore.get().threadId === threadId) historyStore.set(NO_STATEWIRE_HISTORY);\n };\n }, [historyStore, threadId]);\n\n // The row's own visibility is the near-top level: a level, not an edge, so\n // pages chain while the user stays at the top and stop once the prepended\n // content has pushed the row away again.\n useEffect(() => {\n if (!rowEl || !hasOlder || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n const near = entry.isIntersecting;\n if (nearTopRef.current === near) continue;\n nearTopRef.current = near;\n setNearTop(near);\n if (near) setNearTopEpoch((epoch) => epoch + 1);\n }\n },\n {\n root: viewport,\n rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px`,\n },\n );\n observer.observe(rowEl);\n return () => {\n observer.disconnect();\n nearTopRef.current = false;\n setNearTop(false);\n };\n }, [rowEl, hasOlder, viewport]);\n\n return (\n <>\n <KeepReadingPlace viewport={viewport} threadId={threadId} messages={messages} />\n <HistoryEdgeRow\n rowRef={setRowEl}\n hasOlder={hasOlder}\n canLoadOlder={canLoadOlder}\n loading={loading}\n onLoadOlder={loadOlder}\n className={className}\n />\n </>\n );\n};\n"],"names":["useState","useRef","useEffect","useMemo","getExternalStoreMessages","Component","jsx","cn","useStatewireToolOutputs","useAuiState","useContext","AthenaStatewireThreadIdContext","useThreadViewportStore","useThreadViewport","StatewireHistoryContext","useDeepAgentWindowSync","isStatewireChannelTeardown","NO_STATEWIRE_HISTORY","jsxs","Fragment"],"mappings":"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"}
|
|
@@ -2,6 +2,8 @@ import { type DeepAgentHistoryLoadEvent, type DeepAgentToolOutputStore } from ".
|
|
|
2
2
|
import type { FC } from "react";
|
|
3
3
|
/** The attribute `ChatThreadShell` stamps on the scroll viewport. */
|
|
4
4
|
export declare const THREAD_VIEWPORT_ATTR = "data-aui-thread-viewport";
|
|
5
|
+
/** The attribute `ChatThreadShell` stamps on its sticky viewport footer (the composer dock). */
|
|
6
|
+
export declare const THREAD_VIEWPORT_FOOTER_ATTR = "data-aui-thread-viewport-footer";
|
|
5
7
|
/**
|
|
6
8
|
* How far above the viewport the edge row counts as near: the row sits above
|
|
7
9
|
* the first loaded message, so reaching it means the user has scrolled past
|
|
@@ -8,6 +8,7 @@ import { useDeepAgentWindowSync } from "../../agent-runtime/src/use-deep-agent-w
|
|
|
8
8
|
import { isStatewireChannelTeardown } from "../../agent-runtime/src/errors.js";
|
|
9
9
|
import { cn } from "cnfast";
|
|
10
10
|
const THREAD_VIEWPORT_ATTR = "data-aui-thread-viewport";
|
|
11
|
+
const THREAD_VIEWPORT_FOOTER_ATTR = "data-aui-thread-viewport-footer";
|
|
11
12
|
const HISTORY_EDGE_NEAR_MARGIN_PX = 1500;
|
|
12
13
|
const HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY = 2;
|
|
13
14
|
const MESSAGE_SELECTOR = "[data-message-id]";
|
|
@@ -249,6 +250,7 @@ export {
|
|
|
249
250
|
HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY,
|
|
250
251
|
HistoryEdgeRow,
|
|
251
252
|
StatewireHistoryEdge,
|
|
252
|
-
THREAD_VIEWPORT_ATTR
|
|
253
|
+
THREAD_VIEWPORT_ATTR,
|
|
254
|
+
THREAD_VIEWPORT_FOOTER_ATTR
|
|
253
255
|
};
|
|
254
256
|
//# sourceMappingURL=statewire-history-edge.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statewire-history-edge.js","sources":["../../../src/chat-ui/thread/statewire-history-edge.tsx"],"sourcesContent":["import {\n getExternalStoreMessages,\n type ThreadMessage,\n type ThreadViewportState,\n useAuiState,\n useThreadViewport,\n useThreadViewportStore,\n} from \"@assistant-ui/react\";\nimport {\n type DeepAgentHistoryLoadEvent,\n type DeepAgentToolOutputStore,\n isStatewireChannelTeardown,\n useDeepAgentWindowSync,\n} from \"@athenaintel/agent-runtime\";\nimport type { FC } from \"react\";\nimport { Component, useContext, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n NO_STATEWIRE_HISTORY,\n StatewireHistoryContext,\n} from \"../../provider/statewire-history-context\";\nimport { useStatewireToolOutputs } from \"../../provider/statewire-window-context\";\nimport { AthenaStatewireThreadIdContext } from \"../../threads/thread-id-context\";\n\n/** The attribute `ChatThreadShell` stamps on the scroll viewport. */\nexport const THREAD_VIEWPORT_ATTR = \"data-aui-thread-viewport\";\n\n/**\n * How far above the viewport the edge row counts as near: the row sits above\n * the first loaded message, so reaching it means the user has scrolled past\n * everything loaded and the next page should already be on its way. Messages\n * this close to the viewport also count as on screen for cut tool outputs.\n */\nexport const HISTORY_EDGE_NEAR_MARGIN_PX = 1500;\n\n/**\n * Cut tool outputs a transcript reads at once. The reads share the session's\n * command lane with everything the person sends, so a long transcript must\n * not queue dozens of them ahead of a message or a Stop.\n */\nexport const HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY = 2;\n\n/** What assistant-ui's `MessagePrimitive.Root` stamps on every message. */\nconst MESSAGE_SELECTOR = \"[data-message-id]\";\n\nconst NO_MESSAGE_IDS: ReadonlySet<string> = new Set();\n\nfunction sameIds({ a, b }: { a: ReadonlySet<string>; b: ReadonlySet<string> }): boolean {\n if (a.size !== b.size) return false;\n for (const id of a) if (!b.has(id)) return false;\n return true;\n}\n\n/**\n * Which messages sit within {@link HISTORY_EDGE_NEAR_MARGIN_PX} of the\n * viewport, so cut tool outputs are read for what the person can reach. A\n * thread opens at its newest turn; reading every loaded step's output up\n * front sent dozens of reads, each re-rendering the transcript, for steps\n * nobody had scrolled to.\n *\n * `undefined` (every message counts) where nothing can be observed: no\n * viewport around the row, or no `IntersectionObserver`. A message rendered\n * without a `data-message-id` root (a host's own renderer) cannot be observed\n * either, so it counts as before.\n */\nfunction useMessagesNearViewport({\n viewport,\n hasViewport,\n messages,\n}: {\n viewport: HTMLElement | null;\n /** A viewport is (or will be) registered around the row. */\n hasViewport: boolean;\n /** The converted thread: every change may have mounted or remounted a message. */\n messages: readonly ThreadMessage[];\n}): ((messageId: string) => boolean) | undefined {\n const [nearIds, setNearIds] = useState<ReadonlySet<string>>(NO_MESSAGE_IDS);\n const observable = hasViewport && typeof IntersectionObserver !== \"undefined\";\n const trackRef = useRef<(() => void) | null>(null);\n // Every id a rendered element carries, as of the last scan.\n const renderedIdsRef = useRef<ReadonlySet<string>>(NO_MESSAGE_IDS);\n\n useEffect(() => {\n if (!viewport || !observable) return undefined;\n const near = new Set<Element>();\n const observed = new Set<Element>();\n const publish = () => {\n const ids = new Set<string>();\n for (const element of near) {\n const id = element.getAttribute(\"data-message-id\");\n if (id) ids.add(id);\n }\n setNearIds((previous) => (sameIds({ a: previous, b: ids }) ? previous : ids));\n };\n const intersections = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting && entry.target.isConnected) near.add(entry.target);\n else near.delete(entry.target);\n }\n publish();\n },\n { root: viewport, rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px` },\n );\n // A page that folds into a turn gives it a new id: a host keyed by id\n // remounts the turn's element, a host keyed by row (`ChatThreadShell`)\n // keeps it and the element's id changes. So what is observed follows the\n // rendered elements, not the message list, and the near ids are read off\n // those elements again on every scan.\n const track = () => {\n for (const element of observed) {\n if (element.isConnected) continue;\n intersections.unobserve(element);\n observed.delete(element);\n near.delete(element);\n }\n const rendered = new Set<string>();\n for (const element of viewport.querySelectorAll(MESSAGE_SELECTOR)) {\n const id = element.getAttribute(\"data-message-id\");\n if (id) rendered.add(id);\n if (observed.has(element)) continue;\n observed.add(element);\n intersections.observe(element);\n }\n renderedIdsRef.current = rendered;\n publish();\n };\n trackRef.current = track;\n track();\n return () => {\n trackRef.current = null;\n renderedIdsRef.current = NO_MESSAGE_IDS;\n intersections.disconnect();\n setNearIds(NO_MESSAGE_IDS);\n };\n }, [viewport, observable]);\n\n // A changed message list is the trigger to re-scan: its messages render in\n // the same commit, so their elements are in the DOM by this effect (a\n // query, no layout read).\n useEffect(() => {\n trackRef.current?.();\n }, [messages]);\n\n return useMemo(() => {\n if (!observable) return undefined;\n return (messageId: string) => {\n // Not registered yet: nothing is on screen to read for.\n if (viewport === null) return false;\n return nearIds.has(messageId) || !renderedIdsRef.current.has(messageId);\n };\n }, [nearIds, observable, viewport]);\n}\n\n/** The wire message a converted message was built from first (its oldest source). */\nfunction oldestSourceId(message: ThreadMessage): string | undefined {\n const source = getExternalStoreMessages<{ id?: unknown }>(message)[0];\n return typeof source?.id === \"string\" ? source.id : undefined;\n}\n\n/**\n * Whether older history landed ahead of what was loaded: the old first\n * message now sits further down (a page ahead of it), or the first message's\n * old oldest wire message now follows earlier ones (a page that folded into\n * the first loaded turn). A first message replaced in place — a branch picked\n * on it, an edit — is neither, and a run streaming at the tail never touches\n * the first message.\n */\nfunction prependedOlderHistory({\n previous,\n next,\n}: {\n previous: readonly ThreadMessage[];\n next: readonly ThreadMessage[];\n}): boolean {\n const previousFirst = previous[0];\n const nextFirst = next[0];\n if (previousFirst === undefined || nextFirst === undefined || previousFirst === nextFirst) return false;\n if (previousFirst.id !== nextFirst.id && next.some((message, index) => index > 0 && message.id === previousFirst.id)) {\n return true;\n }\n const previousOldest = oldestSourceId(previousFirst);\n if (previousOldest === undefined || previousOldest === oldestSourceId(nextFirst)) return false;\n return getExternalStoreMessages<{ id?: unknown }>(nextFirst).some(\n (source, index) => index > 0 && source?.id === previousOldest,\n );\n}\n\ninterface KeepReadingPlaceProps {\n viewport: HTMLElement | null;\n threadId: string | null;\n /** The converted thread. */\n messages: readonly ThreadMessage[];\n}\n\n/**\n * Keeps the reader's place when older history lands. Everything a page adds\n * sits above what the person is reading, so their distance from the bottom of\n * the transcript is what must survive. A class for `getSnapshotBeforeUpdate`:\n * the distance is read right before React applies the commit that renders the\n * page — the one moment it is exact, even for a scroll the browser has not\n * reported yet — and restored after it, instantly (never through the\n * viewport's smooth scrolling). Other commits read no layout.\n *\n * The browser's scroll anchoring and assistant-ui's prepend anchor aim at the\n * same position, so whichever ran first leaves this nothing to do; on its own\n * it covers what they miss: a reader at the very top (browsers never anchor\n * at scroll offset 0) and a page that folds into the first loaded turn of a\n * host that keys rows by message id (that message remounts under a new id,\n * taking the browser's anchor node with it; `ChatThreadShell` keeps the row).\n */\nclass KeepReadingPlace extends Component<KeepReadingPlaceProps> {\n getSnapshotBeforeUpdate(previous: Readonly<KeepReadingPlaceProps>): number | null {\n const { viewport, threadId, messages } = this.props;\n if (\n viewport === null ||\n viewport !== previous.viewport ||\n threadId !== previous.threadId ||\n messages === previous.messages ||\n !prependedOlderHistory({ previous: previous.messages, next: messages })\n ) {\n return null;\n }\n return viewport.scrollHeight - viewport.scrollTop;\n }\n\n componentDidUpdate(\n _previous: Readonly<KeepReadingPlaceProps>,\n _state: unknown,\n fromBottom: number | null,\n ): void {\n const { viewport } = this.props;\n if (viewport === null || fromBottom === null) return;\n const top = viewport.scrollHeight - fromBottom;\n if (Math.abs(viewport.scrollTop - top) >= 1) viewport.scrollTo({ top, behavior: \"instant\" });\n }\n\n render(): null {\n return null;\n }\n}\n\nexport interface HistoryEdgeRowProps {\n /** Whether the server holds messages older than the loaded window. */\n hasOlder: boolean;\n /** Whether the next page can be asked for; `false` with `hasOlder` is the connection's loading depth spent. Defaults to `hasOlder`. */\n canLoadOlder?: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n onLoadOlder: () => void;\n className?: string;\n /** The row element, for the viewport observer. */\n rowRef?: (element: HTMLElement | null) => void;\n}\n\n/**\n * The top edge of the LOADED history: a manual way to load the next page of\n * older messages, and the in-flight state. Renders nothing when the whole\n * transcript is loaded (or the head interest has not resolved).\n */\nexport const HistoryEdgeRow: FC<HistoryEdgeRowProps> = ({\n hasOlder,\n canLoadOlder = hasOlder,\n loading,\n onLoadOlder,\n className,\n rowRef,\n}) => {\n if (!hasOlder) return null;\n return (\n <div\n ref={rowRef}\n data-testid=\"thread-history-edge\"\n className={cn(\n \"mx-auto flex w-full max-w-(--thread-max-width) items-center justify-center py-1 text-xs text-muted-foreground\",\n className,\n )}\n >\n {loading ? (\n <span data-testid=\"thread-history-edge-loading\">Loading earlier messages…</span>\n ) : canLoadOlder ? (\n <button\n type=\"button\"\n data-testid=\"thread-history-edge-load\"\n onClick={onLoadOlder}\n className=\"rounded-lg px-2.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground\"\n >\n Load earlier messages\n </button>\n ) : (\n <span data-testid=\"thread-history-edge-limit\">Earlier messages can't be loaded here</span>\n )}\n </div>\n );\n};\n\nexport interface StatewireHistoryEdgeProps {\n /**\n * The thread's tool-output store (the one its converter substitutes from).\n * Defaults to `AthenaProvider`'s; hosts mounting the runtime themselves pass\n * the store they gave `useDeepAgentRuntime({ toolOutputs })`.\n */\n toolOutputs?: DeepAgentToolOutputStore;\n className?: string;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: (event: DeepAgentHistoryLoadEvent) => void;\n}\n\n/**\n * The client half of the interest window for a DOM thread — mounted once per\n * thread, inside the scroll viewport above the messages (`ChatThreadShell`'s\n * `beforeMessages` slot):\n *\n * - renders the \"Load earlier messages\" edge row and loads the next page when\n * the row comes near the viewport (scrolling up) or on its button, keeping\n * the reading position when the page lands above the view;\n * - fetches cut tool outputs whole for the cards near the viewport, a few at\n * a time, so they re-render complete through the converter (`toolOutputs`).\n *\n * Inert until the head interest resolves: no row, no reads.\n */\nexport const StatewireHistoryEdge: FC<StatewireHistoryEdgeProps> = ({\n toolOutputs: toolOutputsProp,\n className,\n onHistoryLoad,\n}) => {\n const providerToolOutputs = useStatewireToolOutputs();\n const toolOutputs = toolOutputsProp ?? providerToolOutputs ?? undefined;\n const messages = useAuiState((s) => s.thread.messages);\n const threadId = useContext(AthenaStatewireThreadIdContext);\n const [nearTop, setNearTop] = useState(false);\n const [nearTopEpoch, setNearTopEpoch] = useState(0);\n const nearTopRef = useRef(false);\n const [rowEl, setRowEl] = useState<HTMLElement | null>(null);\n // The scroll viewport the thread registered (assistant-ui's viewport store),\n // known even while the row renders nothing; the row's ancestor otherwise.\n const hasViewportStore = useThreadViewportStore({ optional: true }) !== null;\n const registeredViewport = useThreadViewport({\n optional: true,\n selector: (state: ThreadViewportState) => state.element.viewport,\n });\n const viewport =\n registeredViewport ?? rowEl?.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ?? null;\n const isMessageMounted = useMessagesNearViewport({\n viewport,\n hasViewport: hasViewportStore || viewport !== null,\n messages,\n });\n const historyStore = useContext(StatewireHistoryContext);\n const { hasOlder, canLoadOlder, loading, loadOlder } = useDeepAgentWindowSync({\n messages,\n toolOutputs,\n nearTop,\n nearTopEpoch,\n isMessageMounted,\n maxConcurrentToolOutputs: HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY,\n onHistoryLoad,\n onError: (error, command) => {\n // Leaving the thread settles an in-flight read as lost: lifecycle, not a failure.\n if (error instanceof Error && isStatewireChannelTeardown(error)) return;\n console.warn(`[StatewireHistoryEdge] ${command} failed:`, error);\n },\n });\n\n // Other surfaces of the provider (the workbench's Shell pane) read where the\n // history stands; leaving the thread or unmounting withdraws the report.\n useEffect(() => {\n if (!historyStore || !threadId) return undefined;\n historyStore.set({ threadId, hasOlder, canLoadOlder, loading, loadOlder });\n return undefined;\n }, [historyStore, threadId, hasOlder, canLoadOlder, loading, loadOlder]);\n useEffect(() => {\n if (!historyStore || !threadId) return undefined;\n return () => {\n if (historyStore.get().threadId === threadId) historyStore.set(NO_STATEWIRE_HISTORY);\n };\n }, [historyStore, threadId]);\n\n // The row's own visibility is the near-top level: a level, not an edge, so\n // pages chain while the user stays at the top and stop once the prepended\n // content has pushed the row away again.\n useEffect(() => {\n if (!rowEl || !hasOlder || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n const near = entry.isIntersecting;\n if (nearTopRef.current === near) continue;\n nearTopRef.current = near;\n setNearTop(near);\n if (near) setNearTopEpoch((epoch) => epoch + 1);\n }\n },\n {\n root: viewport,\n rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px`,\n },\n );\n observer.observe(rowEl);\n return () => {\n observer.disconnect();\n nearTopRef.current = false;\n setNearTop(false);\n };\n }, [rowEl, hasOlder, viewport]);\n\n return (\n <>\n <KeepReadingPlace viewport={viewport} threadId={threadId} messages={messages} />\n <HistoryEdgeRow\n rowRef={setRowEl}\n hasOlder={hasOlder}\n canLoadOlder={canLoadOlder}\n loading={loading}\n onLoadOlder={loadOlder}\n className={className}\n />\n </>\n );\n};\n"],"names":[],"mappings":";;;;;;;;;AAyBO,MAAM,uBAAuB;AAQ7B,MAAM,8BAA8B;AAOpC,MAAM,uCAAuC;AAGpD,MAAM,mBAAmB;AAEzB,MAAM,qCAA0C,IAAA;AAEhD,SAAS,QAAQ,EAAE,GAAG,KAAkE;AACtF,MAAI,EAAE,SAAS,EAAE,KAAM,QAAO;AAC9B,aAAW,MAAM,EAAG,KAAI,CAAC,EAAE,IAAI,EAAE,EAAG,QAAO;AAC3C,SAAO;AACT;AAcA,SAAS,wBAAwB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AACF,GAMiD;AAC/C,QAAM,CAAC,SAAS,UAAU,IAAI,SAA8B,cAAc;AAC1E,QAAM,aAAa,eAAe,OAAO,yBAAyB;AAClE,QAAM,WAAW,OAA4B,IAAI;AAEjD,QAAM,iBAAiB,OAA4B,cAAc;AAEjE,YAAU,MAAM;AACd,QAAI,CAAC,YAAY,CAAC,WAAY,QAAO;AACrC,UAAM,2BAAW,IAAA;AACjB,UAAM,+BAAe,IAAA;AACrB,UAAM,UAAU,MAAM;AACpB,YAAM,0BAAU,IAAA;AAChB,iBAAW,WAAW,MAAM;AAC1B,cAAM,KAAK,QAAQ,aAAa,iBAAiB;AACjD,YAAI,GAAI,KAAI,IAAI,EAAE;AAAA,MACpB;AACA,iBAAW,CAAC,aAAc,QAAQ,EAAE,GAAG,UAAU,GAAG,IAAA,CAAK,IAAI,WAAW,GAAI;AAAA,IAC9E;AACA,UAAM,gBAAgB,IAAI;AAAA,MACxB,CAAC,YAAY;AACX,mBAAW,SAAS,SAAS;AAC3B,cAAI,MAAM,kBAAkB,MAAM,OAAO,YAAa,MAAK,IAAI,MAAM,MAAM;AAAA,cACtE,MAAK,OAAO,MAAM,MAAM;AAAA,QAC/B;AACA,gBAAA;AAAA,MACF;AAAA,MACA,EAAE,MAAM,UAAU,YAAY,GAAG,2BAA2B,SAAA;AAAA,IAAS;AAOvE,UAAM,QAAQ,MAAM;AAClB,iBAAW,WAAW,UAAU;AAC9B,YAAI,QAAQ,YAAa;AACzB,sBAAc,UAAU,OAAO;AAC/B,iBAAS,OAAO,OAAO;AACvB,aAAK,OAAO,OAAO;AAAA,MACrB;AACA,YAAM,+BAAe,IAAA;AACrB,iBAAW,WAAW,SAAS,iBAAiB,gBAAgB,GAAG;AACjE,cAAM,KAAK,QAAQ,aAAa,iBAAiB;AACjD,YAAI,GAAI,UAAS,IAAI,EAAE;AACvB,YAAI,SAAS,IAAI,OAAO,EAAG;AAC3B,iBAAS,IAAI,OAAO;AACpB,sBAAc,QAAQ,OAAO;AAAA,MAC/B;AACA,qBAAe,UAAU;AACzB,cAAA;AAAA,IACF;AACA,aAAS,UAAU;AACnB,UAAA;AACA,WAAO,MAAM;AACX,eAAS,UAAU;AACnB,qBAAe,UAAU;AACzB,oBAAc,WAAA;AACd,iBAAW,cAAc;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,UAAU,UAAU,CAAC;AAKzB,YAAU,MAAM;;AACd,mBAAS,YAAT;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAEb,SAAO,QAAQ,MAAM;AACnB,QAAI,CAAC,WAAY,QAAO;AACxB,WAAO,CAAC,cAAsB;AAE5B,UAAI,aAAa,KAAM,QAAO;AAC9B,aAAO,QAAQ,IAAI,SAAS,KAAK,CAAC,eAAe,QAAQ,IAAI,SAAS;AAAA,IACxE;AAAA,EACF,GAAG,CAAC,SAAS,YAAY,QAAQ,CAAC;AACpC;AAGA,SAAS,eAAe,SAA4C;AAClE,QAAM,SAAS,yBAA2C,OAAO,EAAE,CAAC;AACpE,SAAO,QAAO,iCAAQ,QAAO,WAAW,OAAO,KAAK;AACtD;AAUA,SAAS,sBAAsB;AAAA,EAC7B;AAAA,EACA;AACF,GAGY;AACV,QAAM,gBAAgB,SAAS,CAAC;AAChC,QAAM,YAAY,KAAK,CAAC;AACxB,MAAI,kBAAkB,UAAa,cAAc,UAAa,kBAAkB,UAAW,QAAO;AAClG,MAAI,cAAc,OAAO,UAAU,MAAM,KAAK,KAAK,CAAC,SAAS,UAAU,QAAQ,KAAK,QAAQ,OAAO,cAAc,EAAE,GAAG;AACpH,WAAO;AAAA,EACT;AACA,QAAM,iBAAiB,eAAe,aAAa;AACnD,MAAI,mBAAmB,UAAa,mBAAmB,eAAe,SAAS,EAAG,QAAO;AACzF,SAAO,yBAA2C,SAAS,EAAE;AAAA,IAC3D,CAAC,QAAQ,UAAU,QAAQ,MAAK,iCAAQ,QAAO;AAAA,EAAA;AAEnD;AAyBA,MAAM,yBAAyB,UAAiC;AAAA,EAC9D,wBAAwB,UAA0D;AAChF,UAAM,EAAE,UAAU,UAAU,SAAA,IAAa,KAAK;AAC9C,QACE,aAAa,QACb,aAAa,SAAS,YACtB,aAAa,SAAS,YACtB,aAAa,SAAS,YACtB,CAAC,sBAAsB,EAAE,UAAU,SAAS,UAAU,MAAM,SAAA,CAAU,GACtE;AACA,aAAO;AAAA,IACT;AACA,WAAO,SAAS,eAAe,SAAS;AAAA,EAC1C;AAAA,EAEA,mBACE,WACA,QACA,YACM;AACN,UAAM,EAAE,aAAa,KAAK;AAC1B,QAAI,aAAa,QAAQ,eAAe,KAAM;AAC9C,UAAM,MAAM,SAAS,eAAe;AACpC,QAAI,KAAK,IAAI,SAAS,YAAY,GAAG,KAAK,EAAG,UAAS,SAAS,EAAE,KAAK,UAAU,WAAW;AAAA,EAC7F;AAAA,EAEA,SAAe;AACb,WAAO;AAAA,EACT;AACF;AAoBO,MAAM,iBAA0C,CAAC;AAAA,EACtD;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,MAAI,CAAC,SAAU,QAAO;AACtB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MACL,eAAY;AAAA,MACZ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAGD,oBACC,oBAAC,QAAA,EAAK,eAAY,+BAA8B,UAAA,6BAAyB,IACvE,eACF;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,eAAY;AAAA,UACZ,SAAS;AAAA,UACT,WAAU;AAAA,UACX,UAAA;AAAA,QAAA;AAAA,MAAA,IAID,oBAAC,QAAA,EAAK,eAAY,6BAA4B,UAAA,wCAAA,CAAqC;AAAA,IAAA;AAAA,EAAA;AAI3F;AA2BO,MAAM,uBAAsD,CAAC;AAAA,EAClE,aAAa;AAAA,EACb;AAAA,EACA;AACF,MAAM;AACJ,QAAM,sBAAsB,wBAAA;AAC5B,QAAM,cAAc,mBAAmB,uBAAuB;AAC9D,QAAM,WAAW,YAAY,CAAC,MAAM,EAAE,OAAO,QAAQ;AACrD,QAAM,WAAW,WAAW,8BAA8B;AAC1D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,CAAC;AAClD,QAAM,aAAa,OAAO,KAAK;AAC/B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA6B,IAAI;AAG3D,QAAM,mBAAmB,uBAAuB,EAAE,UAAU,KAAA,CAAM,MAAM;AACxE,QAAM,qBAAqB,kBAAkB;AAAA,IAC3C,UAAU;AAAA,IACV,UAAU,CAAC,UAA+B,MAAM,QAAQ;AAAA,EAAA,CACzD;AACD,QAAM,WACJ,uBAAsB,+BAAO,QAAqB,IAAI,oBAAoB,SAAQ;AACpF,QAAM,mBAAmB,wBAAwB;AAAA,IAC/C;AAAA,IACA,aAAa,oBAAoB,aAAa;AAAA,IAC9C;AAAA,EAAA,CACD;AACD,QAAM,eAAe,WAAW,uBAAuB;AACvD,QAAM,EAAE,UAAU,cAAc,SAAS,UAAA,IAAc,uBAAuB;AAAA,IAC5E;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,0BAA0B;AAAA,IAC1B;AAAA,IACA,SAAS,CAAC,OAAO,YAAY;AAE3B,UAAI,iBAAiB,SAAS,2BAA2B,KAAK,EAAG;AACjE,cAAQ,KAAK,0BAA0B,OAAO,YAAY,KAAK;AAAA,IACjE;AAAA,EAAA,CACD;AAID,YAAU,MAAM;AACd,QAAI,CAAC,gBAAgB,CAAC,SAAU,QAAO;AACvC,iBAAa,IAAI,EAAE,UAAU,UAAU,cAAc,SAAS,WAAW;AACzE,WAAO;AAAA,EACT,GAAG,CAAC,cAAc,UAAU,UAAU,cAAc,SAAS,SAAS,CAAC;AACvE,YAAU,MAAM;AACd,QAAI,CAAC,gBAAgB,CAAC,SAAU,QAAO;AACvC,WAAO,MAAM;AACX,UAAI,aAAa,MAAM,aAAa,SAAU,cAAa,IAAI,oBAAoB;AAAA,IACrF;AAAA,EACF,GAAG,CAAC,cAAc,QAAQ,CAAC;AAK3B,YAAU,MAAM;AACd,QAAI,CAAC,SAAS,CAAC,YAAY,OAAO,yBAAyB,YAAa;AACxE,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,YAAY;AACX,mBAAW,SAAS,SAAS;AAC3B,gBAAM,OAAO,MAAM;AACnB,cAAI,WAAW,YAAY,KAAM;AACjC,qBAAW,UAAU;AACrB,qBAAW,IAAI;AACf,cAAI,KAAM,iBAAgB,CAAC,UAAU,QAAQ,CAAC;AAAA,QAChD;AAAA,MACF;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,YAAY,GAAG,2BAA2B;AAAA,MAAA;AAAA,IAC5C;AAEF,aAAS,QAAQ,KAAK;AACtB,WAAO,MAAM;AACX,eAAS,WAAA;AACT,iBAAW,UAAU;AACrB,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,OAAO,UAAU,QAAQ,CAAC;AAE9B,SACE,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA,oBAAC,kBAAA,EAAiB,UAAoB,UAAoB,SAAA,CAAoB;AAAA,IAC9E;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA,aAAa;AAAA,QACb;AAAA,MAAA;AAAA,IAAA;AAAA,EACF,GACF;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"statewire-history-edge.js","sources":["../../../src/chat-ui/thread/statewire-history-edge.tsx"],"sourcesContent":["import {\n getExternalStoreMessages,\n type ThreadMessage,\n type ThreadViewportState,\n useAuiState,\n useThreadViewport,\n useThreadViewportStore,\n} from \"@assistant-ui/react\";\nimport {\n type DeepAgentHistoryLoadEvent,\n type DeepAgentToolOutputStore,\n isStatewireChannelTeardown,\n useDeepAgentWindowSync,\n} from \"@athenaintel/agent-runtime\";\nimport type { FC } from \"react\";\nimport { Component, useContext, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n NO_STATEWIRE_HISTORY,\n StatewireHistoryContext,\n} from \"../../provider/statewire-history-context\";\nimport { useStatewireToolOutputs } from \"../../provider/statewire-window-context\";\nimport { AthenaStatewireThreadIdContext } from \"../../threads/thread-id-context\";\n\n/** The attribute `ChatThreadShell` stamps on the scroll viewport. */\nexport const THREAD_VIEWPORT_ATTR = \"data-aui-thread-viewport\";\n/** The attribute `ChatThreadShell` stamps on its sticky viewport footer (the composer dock). */\nexport const THREAD_VIEWPORT_FOOTER_ATTR = \"data-aui-thread-viewport-footer\";\n\n/**\n * How far above the viewport the edge row counts as near: the row sits above\n * the first loaded message, so reaching it means the user has scrolled past\n * everything loaded and the next page should already be on its way. Messages\n * this close to the viewport also count as on screen for cut tool outputs.\n */\nexport const HISTORY_EDGE_NEAR_MARGIN_PX = 1500;\n\n/**\n * Cut tool outputs a transcript reads at once. The reads share the session's\n * command lane with everything the person sends, so a long transcript must\n * not queue dozens of them ahead of a message or a Stop.\n */\nexport const HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY = 2;\n\n/** What assistant-ui's `MessagePrimitive.Root` stamps on every message. */\nconst MESSAGE_SELECTOR = \"[data-message-id]\";\n\nconst NO_MESSAGE_IDS: ReadonlySet<string> = new Set();\n\nfunction sameIds({ a, b }: { a: ReadonlySet<string>; b: ReadonlySet<string> }): boolean {\n if (a.size !== b.size) return false;\n for (const id of a) if (!b.has(id)) return false;\n return true;\n}\n\n/**\n * Which messages sit within {@link HISTORY_EDGE_NEAR_MARGIN_PX} of the\n * viewport, so cut tool outputs are read for what the person can reach. A\n * thread opens at its newest turn; reading every loaded step's output up\n * front sent dozens of reads, each re-rendering the transcript, for steps\n * nobody had scrolled to.\n *\n * `undefined` (every message counts) where nothing can be observed: no\n * viewport around the row, or no `IntersectionObserver`. A message rendered\n * without a `data-message-id` root (a host's own renderer) cannot be observed\n * either, so it counts as before.\n */\nfunction useMessagesNearViewport({\n viewport,\n hasViewport,\n messages,\n}: {\n viewport: HTMLElement | null;\n /** A viewport is (or will be) registered around the row. */\n hasViewport: boolean;\n /** The converted thread: every change may have mounted or remounted a message. */\n messages: readonly ThreadMessage[];\n}): ((messageId: string) => boolean) | undefined {\n const [nearIds, setNearIds] = useState<ReadonlySet<string>>(NO_MESSAGE_IDS);\n const observable = hasViewport && typeof IntersectionObserver !== \"undefined\";\n const trackRef = useRef<(() => void) | null>(null);\n // Every id a rendered element carries, as of the last scan.\n const renderedIdsRef = useRef<ReadonlySet<string>>(NO_MESSAGE_IDS);\n\n useEffect(() => {\n if (!viewport || !observable) return undefined;\n const near = new Set<Element>();\n const observed = new Set<Element>();\n const publish = () => {\n const ids = new Set<string>();\n for (const element of near) {\n const id = element.getAttribute(\"data-message-id\");\n if (id) ids.add(id);\n }\n setNearIds((previous) => (sameIds({ a: previous, b: ids }) ? previous : ids));\n };\n const intersections = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting && entry.target.isConnected) near.add(entry.target);\n else near.delete(entry.target);\n }\n publish();\n },\n { root: viewport, rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px` },\n );\n // A page that folds into a turn gives it a new id: a host keyed by id\n // remounts the turn's element, a host keyed by row (`ChatThreadShell`)\n // keeps it and the element's id changes. So what is observed follows the\n // rendered elements, not the message list, and the near ids are read off\n // those elements again on every scan.\n const track = () => {\n for (const element of observed) {\n if (element.isConnected) continue;\n intersections.unobserve(element);\n observed.delete(element);\n near.delete(element);\n }\n const rendered = new Set<string>();\n for (const element of viewport.querySelectorAll(MESSAGE_SELECTOR)) {\n const id = element.getAttribute(\"data-message-id\");\n if (id) rendered.add(id);\n if (observed.has(element)) continue;\n observed.add(element);\n intersections.observe(element);\n }\n renderedIdsRef.current = rendered;\n publish();\n };\n trackRef.current = track;\n track();\n return () => {\n trackRef.current = null;\n renderedIdsRef.current = NO_MESSAGE_IDS;\n intersections.disconnect();\n setNearIds(NO_MESSAGE_IDS);\n };\n }, [viewport, observable]);\n\n // A changed message list is the trigger to re-scan: its messages render in\n // the same commit, so their elements are in the DOM by this effect (a\n // query, no layout read).\n useEffect(() => {\n trackRef.current?.();\n }, [messages]);\n\n return useMemo(() => {\n if (!observable) return undefined;\n return (messageId: string) => {\n // Not registered yet: nothing is on screen to read for.\n if (viewport === null) return false;\n return nearIds.has(messageId) || !renderedIdsRef.current.has(messageId);\n };\n }, [nearIds, observable, viewport]);\n}\n\n/** The wire message a converted message was built from first (its oldest source). */\nfunction oldestSourceId(message: ThreadMessage): string | undefined {\n const source = getExternalStoreMessages<{ id?: unknown }>(message)[0];\n return typeof source?.id === \"string\" ? source.id : undefined;\n}\n\n/**\n * Whether older history landed ahead of what was loaded: the old first\n * message now sits further down (a page ahead of it), or the first message's\n * old oldest wire message now follows earlier ones (a page that folded into\n * the first loaded turn). A first message replaced in place — a branch picked\n * on it, an edit — is neither, and a run streaming at the tail never touches\n * the first message.\n */\nfunction prependedOlderHistory({\n previous,\n next,\n}: {\n previous: readonly ThreadMessage[];\n next: readonly ThreadMessage[];\n}): boolean {\n const previousFirst = previous[0];\n const nextFirst = next[0];\n if (previousFirst === undefined || nextFirst === undefined || previousFirst === nextFirst) return false;\n if (previousFirst.id !== nextFirst.id && next.some((message, index) => index > 0 && message.id === previousFirst.id)) {\n return true;\n }\n const previousOldest = oldestSourceId(previousFirst);\n if (previousOldest === undefined || previousOldest === oldestSourceId(nextFirst)) return false;\n return getExternalStoreMessages<{ id?: unknown }>(nextFirst).some(\n (source, index) => index > 0 && source?.id === previousOldest,\n );\n}\n\ninterface KeepReadingPlaceProps {\n viewport: HTMLElement | null;\n threadId: string | null;\n /** The converted thread. */\n messages: readonly ThreadMessage[];\n}\n\n/**\n * Keeps the reader's place when older history lands. Everything a page adds\n * sits above what the person is reading, so their distance from the bottom of\n * the transcript is what must survive. A class for `getSnapshotBeforeUpdate`:\n * the distance is read right before React applies the commit that renders the\n * page — the one moment it is exact, even for a scroll the browser has not\n * reported yet — and restored after it, instantly (never through the\n * viewport's smooth scrolling). Other commits read no layout.\n *\n * The browser's scroll anchoring and assistant-ui's prepend anchor aim at the\n * same position, so whichever ran first leaves this nothing to do; on its own\n * it covers what they miss: a reader at the very top (browsers never anchor\n * at scroll offset 0) and a page that folds into the first loaded turn of a\n * host that keys rows by message id (that message remounts under a new id,\n * taking the browser's anchor node with it; `ChatThreadShell` keeps the row).\n */\nclass KeepReadingPlace extends Component<KeepReadingPlaceProps> {\n getSnapshotBeforeUpdate(previous: Readonly<KeepReadingPlaceProps>): number | null {\n const { viewport, threadId, messages } = this.props;\n if (\n viewport === null ||\n viewport !== previous.viewport ||\n threadId !== previous.threadId ||\n messages === previous.messages ||\n !prependedOlderHistory({ previous: previous.messages, next: messages })\n ) {\n return null;\n }\n return viewport.scrollHeight - viewport.scrollTop;\n }\n\n componentDidUpdate(\n _previous: Readonly<KeepReadingPlaceProps>,\n _state: unknown,\n fromBottom: number | null,\n ): void {\n const { viewport } = this.props;\n if (viewport === null || fromBottom === null) return;\n const top = viewport.scrollHeight - fromBottom;\n if (Math.abs(viewport.scrollTop - top) >= 1) viewport.scrollTo({ top, behavior: \"instant\" });\n }\n\n render(): null {\n return null;\n }\n}\n\nexport interface HistoryEdgeRowProps {\n /** Whether the server holds messages older than the loaded window. */\n hasOlder: boolean;\n /** Whether the next page can be asked for; `false` with `hasOlder` is the connection's loading depth spent. Defaults to `hasOlder`. */\n canLoadOlder?: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n onLoadOlder: () => void;\n className?: string;\n /** The row element, for the viewport observer. */\n rowRef?: (element: HTMLElement | null) => void;\n}\n\n/**\n * The top edge of the LOADED history: a manual way to load the next page of\n * older messages, and the in-flight state. Renders nothing when the whole\n * transcript is loaded (or the head interest has not resolved).\n */\nexport const HistoryEdgeRow: FC<HistoryEdgeRowProps> = ({\n hasOlder,\n canLoadOlder = hasOlder,\n loading,\n onLoadOlder,\n className,\n rowRef,\n}) => {\n if (!hasOlder) return null;\n return (\n <div\n ref={rowRef}\n data-testid=\"thread-history-edge\"\n className={cn(\n \"mx-auto flex w-full max-w-(--thread-max-width) items-center justify-center py-1 text-xs text-muted-foreground\",\n className,\n )}\n >\n {loading ? (\n <span data-testid=\"thread-history-edge-loading\">Loading earlier messages…</span>\n ) : canLoadOlder ? (\n <button\n type=\"button\"\n data-testid=\"thread-history-edge-load\"\n onClick={onLoadOlder}\n className=\"rounded-lg px-2.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground\"\n >\n Load earlier messages\n </button>\n ) : (\n <span data-testid=\"thread-history-edge-limit\">Earlier messages can't be loaded here</span>\n )}\n </div>\n );\n};\n\nexport interface StatewireHistoryEdgeProps {\n /**\n * The thread's tool-output store (the one its converter substitutes from).\n * Defaults to `AthenaProvider`'s; hosts mounting the runtime themselves pass\n * the store they gave `useDeepAgentRuntime({ toolOutputs })`.\n */\n toolOutputs?: DeepAgentToolOutputStore;\n className?: string;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: (event: DeepAgentHistoryLoadEvent) => void;\n}\n\n/**\n * The client half of the interest window for a DOM thread — mounted once per\n * thread, inside the scroll viewport above the messages (`ChatThreadShell`'s\n * `beforeMessages` slot):\n *\n * - renders the \"Load earlier messages\" edge row and loads the next page when\n * the row comes near the viewport (scrolling up) or on its button, keeping\n * the reading position when the page lands above the view;\n * - fetches cut tool outputs whole for the cards near the viewport, a few at\n * a time, so they re-render complete through the converter (`toolOutputs`).\n *\n * Inert until the head interest resolves: no row, no reads.\n */\nexport const StatewireHistoryEdge: FC<StatewireHistoryEdgeProps> = ({\n toolOutputs: toolOutputsProp,\n className,\n onHistoryLoad,\n}) => {\n const providerToolOutputs = useStatewireToolOutputs();\n const toolOutputs = toolOutputsProp ?? providerToolOutputs ?? undefined;\n const messages = useAuiState((s) => s.thread.messages);\n const threadId = useContext(AthenaStatewireThreadIdContext);\n const [nearTop, setNearTop] = useState(false);\n const [nearTopEpoch, setNearTopEpoch] = useState(0);\n const nearTopRef = useRef(false);\n const [rowEl, setRowEl] = useState<HTMLElement | null>(null);\n // The scroll viewport the thread registered (assistant-ui's viewport store),\n // known even while the row renders nothing; the row's ancestor otherwise.\n const hasViewportStore = useThreadViewportStore({ optional: true }) !== null;\n const registeredViewport = useThreadViewport({\n optional: true,\n selector: (state: ThreadViewportState) => state.element.viewport,\n });\n const viewport =\n registeredViewport ?? rowEl?.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ?? null;\n const isMessageMounted = useMessagesNearViewport({\n viewport,\n hasViewport: hasViewportStore || viewport !== null,\n messages,\n });\n const historyStore = useContext(StatewireHistoryContext);\n const { hasOlder, canLoadOlder, loading, loadOlder } = useDeepAgentWindowSync({\n messages,\n toolOutputs,\n nearTop,\n nearTopEpoch,\n isMessageMounted,\n maxConcurrentToolOutputs: HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY,\n onHistoryLoad,\n onError: (error, command) => {\n // Leaving the thread settles an in-flight read as lost: lifecycle, not a failure.\n if (error instanceof Error && isStatewireChannelTeardown(error)) return;\n console.warn(`[StatewireHistoryEdge] ${command} failed:`, error);\n },\n });\n\n // Other surfaces of the provider (the workbench's Shell pane) read where the\n // history stands; leaving the thread or unmounting withdraws the report.\n useEffect(() => {\n if (!historyStore || !threadId) return undefined;\n historyStore.set({ threadId, hasOlder, canLoadOlder, loading, loadOlder });\n return undefined;\n }, [historyStore, threadId, hasOlder, canLoadOlder, loading, loadOlder]);\n useEffect(() => {\n if (!historyStore || !threadId) return undefined;\n return () => {\n if (historyStore.get().threadId === threadId) historyStore.set(NO_STATEWIRE_HISTORY);\n };\n }, [historyStore, threadId]);\n\n // The row's own visibility is the near-top level: a level, not an edge, so\n // pages chain while the user stays at the top and stop once the prepended\n // content has pushed the row away again.\n useEffect(() => {\n if (!rowEl || !hasOlder || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n const near = entry.isIntersecting;\n if (nearTopRef.current === near) continue;\n nearTopRef.current = near;\n setNearTop(near);\n if (near) setNearTopEpoch((epoch) => epoch + 1);\n }\n },\n {\n root: viewport,\n rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px`,\n },\n );\n observer.observe(rowEl);\n return () => {\n observer.disconnect();\n nearTopRef.current = false;\n setNearTop(false);\n };\n }, [rowEl, hasOlder, viewport]);\n\n return (\n <>\n <KeepReadingPlace viewport={viewport} threadId={threadId} messages={messages} />\n <HistoryEdgeRow\n rowRef={setRowEl}\n hasOlder={hasOlder}\n canLoadOlder={canLoadOlder}\n loading={loading}\n onLoadOlder={loadOlder}\n className={className}\n />\n </>\n );\n};\n"],"names":[],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -57,6 +57,7 @@ const miscToolUis = require("./tools/misc-tool-uis.cjs");
|
|
|
57
57
|
const toolIcons = require("./tools/tool-icons.cjs");
|
|
58
58
|
const writeTodosToolUi = require("./tools/write-todos-tool-ui.cjs");
|
|
59
59
|
const todoPlanStore = require("./tools/todo-plan-store.cjs");
|
|
60
|
+
const AthenaPlanStrip = require("./chat/AthenaPlanStrip.cjs");
|
|
60
61
|
const fileToolUis = require("./tools/file-tool-uis.cjs");
|
|
61
62
|
const toolSummary = require("./tools/tool-summary.cjs");
|
|
62
63
|
const toolFallback = require("./tools/tool-fallback.cjs");
|
|
@@ -297,6 +298,7 @@ exports.PlanStepMarker = writeTodosToolUi.PlanStepMarker;
|
|
|
297
298
|
exports.WriteTodosToolUI = writeTodosToolUi.WriteTodosToolUI;
|
|
298
299
|
exports.parseTodoPlan = writeTodosToolUi.parseTodoPlan;
|
|
299
300
|
exports.useLatestTodoPlan = todoPlanStore.useLatestTodoPlan;
|
|
301
|
+
exports.AthenaPlanStrip = AthenaPlanStrip.AthenaPlanStrip;
|
|
300
302
|
exports.EditFileToolUI = fileToolUis.EditFileToolUI;
|
|
301
303
|
exports.ListFilesToolUI = fileToolUis.ListFilesToolUI;
|
|
302
304
|
exports.ReadFileToolUI = fileToolUis.ReadFileToolUI;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export { AthenaAssistantActionBar, AthenaAssistantMessage, AthenaChat, AthenaRea
|
|
|
20
20
|
export type { AthenaAssistantActionBarProps, AthenaAssistantMessageProps, AthenaChatComponents, AthenaChatMessagePartComponents, AthenaChatProps, AthenaChatVoiceOptions, AthenaReasoningPartProps, AthenaUserMessageProps, } from './chat/AthenaChat';
|
|
21
21
|
export type { WelcomeSuggestion } from './chat/athena-chat-welcome';
|
|
22
22
|
export { AthenaChatErrorBoundary } from './chat/AthenaChatErrorBoundary';
|
|
23
|
-
export { type ActivityItem, type ActivityPhase, type ActivityTimelineOptions, type ActivityToolState, buildActivityTimeline, CollapsibleGroup, type CollapsibleGroupProps, countActivityDelegations, countActivityTools, createToolBasedGroupingFunction, DEFAULT_TOOLKIT_THEME, defaultGetToolGroupKey, excludeFinalTextFromGroups, excludePartsFromGroups, type FinalTextPart, type FinalTextSegment, formatWorkDuration, getActivityItemLabel, getActivitySummary, getActivityToolState, getFinalText, getFinalTextParts, getReasoningLabel, type GetToolGroupKey, type GroupablePart, GroupedMessageParts, type GroupedMessagePartsProps, type GroupingFunction, type MessagePartGroup, readTurnTiming, resetRememberedExpansionsForTests, resolveActivityPhase, splitTurnSegments, SuperGroupingCard, type SuperGroupingCardProps, SuperGroupingFinalText, SuperGroupingProvider, SuperGroupingTranscript, type SuperGroupingTranscriptProps, TOOLKIT_THEMES, type ToolGroupKeyOptions, type ToolkitTheme, type ToolPartPlacement, ToolPartPlacementProvider, type TurnSegment, type TurnTiming, useActivityExpansion, useToolPartPlacement, workedDurationMs, } from './chat/super-grouping';
|
|
23
|
+
export { type ActivityItem, type ActivityPhase, type ActivityTimelineOptions, type ActivityToolState, buildActivityTimeline, CollapsibleGroup, type CollapsibleGroupProps, countActivityDelegations, countActivityTools, createToolBasedGroupingFunction, DEFAULT_TOOLKIT_THEME, defaultGetToolGroupKey, excludeFinalTextFromGroups, excludePartsFromGroups, type FinalTextPart, type FinalTextSegment, formatWorkDuration, type ActivitySummaryOptions, getActivityItemLabel, getActivitySummary, getActivityToolState, getFinalText, getFinalTextParts, getReasoningLabel, type GetToolGroupKey, type GroupablePart, GroupedMessageParts, type GroupedMessagePartsProps, type GroupingFunction, type MessagePartGroup, readTurnTiming, resetRememberedExpansionsForTests, resolveActivityPhase, splitTurnSegments, SuperGroupingCard, type SuperGroupingCardProps, SuperGroupingFinalText, SuperGroupingProvider, SuperGroupingTranscript, type SuperGroupingTranscriptProps, TOOLKIT_THEMES, type ToolGroupKeyOptions, type ToolkitTheme, type ToolPartPlacement, ToolPartPlacementProvider, type TurnSegment, type TurnTiming, useActivityExpansion, useToolPartPlacement, workedDurationMs, } from './chat/super-grouping';
|
|
24
24
|
export { StatewireApprovalCard } from './chat/StatewireApprovalCard';
|
|
25
25
|
export type { StatewireApprovalCardProps, StatewireApprovalEditorContext, StatewireApprovalRequestContext, StatewireDecisionAnswer, StatewireDecisionConfirmer, } from './chat/StatewireApprovalCard';
|
|
26
26
|
export { useStatewireApprovalRequest } from './chat/use-statewire-approval-request';
|
|
@@ -85,6 +85,7 @@ export { classifyToolResult, describeToolArgs, getToolDisplayMeta, isToolResultS
|
|
|
85
85
|
export type { ToolDisplayMeta, ToolIconKey, ToolResultClassification, } from './agent-runtime/src/index';
|
|
86
86
|
export { HiddenWriteTodosToolUI, PlanStepMarker, WriteTodosToolUI, parseTodoPlan, } from './tools/write-todos-tool-ui';
|
|
87
87
|
export { useLatestTodoPlan } from './tools/todo-plan-store';
|
|
88
|
+
export { AthenaPlanStrip, type AthenaPlanStripProps } from './chat/AthenaPlanStrip';
|
|
88
89
|
export type { LatestTodoPlan } from './tools/todo-plan-store';
|
|
89
90
|
export { EditFileToolUI, friendlyWorkspacePath, ListFilesToolUI, ReadFileToolUI, SearchFilesToolUI, WriteFileToolUI, } from './tools/file-tool-uis';
|
|
90
91
|
export { collectAssetTitles, humanizeToolName, summarizeToolCall, type SummarizeToolCallInput, type ToolCallSummary, type ToolSummaryState, } from './tools/tool-summary';
|
package/dist/index.js
CHANGED
|
@@ -55,6 +55,7 @@ import { InstallPackageToolUI } from "./tools/misc-tool-uis.js";
|
|
|
55
55
|
import { TOOL_ICONS, toolIcon } from "./tools/tool-icons.js";
|
|
56
56
|
import { HiddenWriteTodosToolUI, PlanStepMarker, WriteTodosToolUI, parseTodoPlan } from "./tools/write-todos-tool-ui.js";
|
|
57
57
|
import { useLatestTodoPlan } from "./tools/todo-plan-store.js";
|
|
58
|
+
import { AthenaPlanStrip } from "./chat/AthenaPlanStrip.js";
|
|
58
59
|
import { EditFileToolUI, ListFilesToolUI, ReadFileToolUI, SearchFilesToolUI, WriteFileToolUI } from "./tools/file-tool-uis.js";
|
|
59
60
|
import { collectAssetTitles, friendlyWorkspacePath, humanizeToolName, summarizeToolCall } from "./tools/tool-summary.js";
|
|
60
61
|
import { ToolFallback, ToolFallbackAppearanceProvider, ToolFallbackArgs, ToolFallbackContent, ToolFallbackError, ToolFallbackNotice, ToolFallbackResult, ToolFallbackRoot, ToolFallbackTrigger, clearAutoOpenedAssets } from "./tools/tool-fallback.js";
|
|
@@ -158,6 +159,7 @@ export {
|
|
|
158
159
|
AthenaChat,
|
|
159
160
|
AthenaChatErrorBoundary,
|
|
160
161
|
AthenaLayout,
|
|
162
|
+
AthenaPlanStrip,
|
|
161
163
|
AthenaProvider,
|
|
162
164
|
AthenaReasoningPart,
|
|
163
165
|
AthenaSdkError,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
6
6
|
const Y = require("@y/y");
|
|
7
7
|
const updateAttributions = require("./update-attributions.cjs");
|
|
8
8
|
const MESSAGE_SYNC = 0;
|
|
9
|
+
const SYNC_STEP_1 = 0;
|
|
9
10
|
const SYNC_STEP_2 = 1;
|
|
10
11
|
const SYNC_UPDATE = 2;
|
|
11
12
|
const MAX_ATTRIBUTED_RUNS = 64;
|
|
@@ -37,6 +38,13 @@ function syncFrame({ syncType, payload }) {
|
|
|
37
38
|
frame.set(payload, head.length);
|
|
38
39
|
return frame;
|
|
39
40
|
}
|
|
41
|
+
function namesNoClient(stateVector) {
|
|
42
|
+
try {
|
|
43
|
+
return Y.decodeStateVector(stateVector).size === 0;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
40
48
|
function idsOf(update) {
|
|
41
49
|
try {
|
|
42
50
|
return Y.createContentIdsFromUpdate(update);
|
|
@@ -99,19 +107,23 @@ class UnconfirmedUpdateAttributions {
|
|
|
99
107
|
* One socket of the room. While the room keeps writes, the socket's sync step 2 (its answer to
|
|
100
108
|
* Keryx's sync step 1) waits for the next frame Keryx sends, its whole document, which drops what
|
|
101
109
|
* Keryx already holds before the answer is split (see the module comment); every frame sent
|
|
102
|
-
* meanwhile waits behind it, in order.
|
|
103
|
-
*
|
|
110
|
+
* meanwhile waits behind it, in order. When Keryx's sync step 1 names no client, the room is new
|
|
111
|
+
* to Keryx: its answer goes out whole, born as one write, and what the room kept waits for its
|
|
112
|
+
* echo. Nothing kept, or a socket that cannot hear Keryx: every frame goes out as it is sent.
|
|
104
113
|
*/
|
|
105
114
|
connect({
|
|
106
115
|
send,
|
|
107
116
|
hears
|
|
108
117
|
}) {
|
|
109
118
|
let held = null;
|
|
119
|
+
let roomIsNew = false;
|
|
110
120
|
const sendNow = (frame) => {
|
|
111
121
|
for (const out of this.framesFor(frame)) send(out);
|
|
112
122
|
};
|
|
113
123
|
return {
|
|
114
124
|
received: (frame) => {
|
|
125
|
+
const message = readSyncFrame(frame);
|
|
126
|
+
if ((message == null ? void 0 : message.syncType) === SYNC_STEP_1) roomIsNew = namesNoClient(message.payload);
|
|
115
127
|
this.received(frame);
|
|
116
128
|
if (held === null) return;
|
|
117
129
|
const waiting = held;
|
|
@@ -124,7 +136,12 @@ class UnconfirmedUpdateAttributions {
|
|
|
124
136
|
held.push(frame);
|
|
125
137
|
return;
|
|
126
138
|
}
|
|
127
|
-
|
|
139
|
+
const syncType = (_a = readSyncFrame(frame)) == null ? void 0 : _a.syncType;
|
|
140
|
+
if (roomIsNew && syncType === SYNC_STEP_2) {
|
|
141
|
+
send(frame);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (hears && this.runs.length > 0 && syncType === SYNC_STEP_2) {
|
|
128
145
|
held = [frame];
|
|
129
146
|
return;
|
|
130
147
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unconfirmed-update-attributions.cjs","sources":["../../../../keryx-http/src/unconfirmed-update-attributions.ts"],"sourcesContent":["/**\n * The attributed writes a room has not seen Keryx acknowledge, kept so that a write made while the\n * room's socket is closed still reaches Keryx with its attributions.\n *\n * A write made inside `runWithDocUpdateAttributions` leaves as its own sync update followed by the\n * attributions trailer (`attributingWebSocket`), but only if the room's socket is open as it is\n * made. One made while it is closed (offline, reconnecting, before the first connection) reaches\n * Keryx inside the reconnect's sync step 2: the client's answer to Keryx's sync step 1, everything\n * it holds that Keryx lacks. Keryx reads no trailer after a sync step 2, so it would count the\n * write as the person's edit.\n *\n * So the room keeps the content ids of every write made in a scope, with the scope's attributions,\n * until Keryx sends those ids back: the echo of the update that carried them, or the whole document\n * Keryx sends when a socket opens (`received`; `@y/websocket` confirms its own unconfirmed updates\n * the same way). When the socket sends a sync step 2, each kept write's part of it goes out first,\n * oldest first, as its own sync update with its trailer, and the sync step 2 follows without those\n * ids (`framesFor`). The parts are cut from the sync step 2 itself, which already holds only what\n * Keryx lacks past its state vector. One socket keeps the order, so Keryx appends each marked update\n * before the person's sync step 2.\n *\n * That answer waits for Keryx's document (`connect`). Keryx sends its sync step 1 and, in the same\n * cork, its whole document, but `@y/websocket` answers the sync step 1 as it arrives. A kept write\n * Keryx already appended (its echo lost with the socket) is still kept then, and the sync step 2\n * holds the client's whole delete set, so its deletes would go out again as a marked update, which\n * Keryx appends: it drops what it holds from the first sync step 2 only. Held until the next frame,\n * the answer is split once Keryx's document has dropped what it holds.\n *\n * What Keryx records (`patches/@y%2Fhub@0.10.1.patch`):\n * - An update's content map attributes every id it carries, inserts and deletes, read from its\n * bytes: an update that cannot integrate yet (an item next to one the person typed offline, the\n * next clocks of the person's own client) is attributed like any other. Peers hold it until the\n * sync step 2 behind it brings what it needs.\n * - An id is attributed by the first stream entry that carries it (`getDoc`'s `excludeContentMap`),\n * so a later copy changes no merged history. The live change notices read each entry's own\n * content map, though (keryx/src/doc-watcher.ts), and the first sync step 2's strip of what Keryx\n * holds reads the known ids when the frame arrives, possibly before the update ahead of it is in\n * the stream (one whose content map the compute pool builds). A copy inside the sync step 2\n * would then be announced as the person's: the sync step 2 never carries a kept write's ids.\n *\n * Bounded: consecutive writes of one scope merge into one run, and past\n * {@link MAX_ATTRIBUTED_RUNS} runs the oldest is let go (its content then reaches Keryx in a sync\n * step 2, as the person's). A room's local copy keeps them across sessions\n * ({@link unconfirmedAttributedWritesOf}, {@link restoreUnconfirmedAttributedWrites}); without one,\n * a write the room kept is forgotten with the page.\n */\nimport {\n type ContentIds,\n createContentIdsFromUpdate,\n type Doc,\n excludeContentIds,\n intersectContentIds,\n intersectUpdateWithContentIds,\n mergeContentIds,\n} from '@y/y';\nimport {\n type AttributedWritesSocket,\n readVarUint,\n type UnconfirmedAttributedWrites,\n type UpdateAttribution,\n withUpdateAttributionsTrailer,\n writeVarUint,\n} from './update-attributions.js';\n\n/** y-protocols: a sync message, and the sub-types that carry content. */\nconst MESSAGE_SYNC = 0;\nconst SYNC_STEP_2 = 1;\nconst SYNC_UPDATE = 2;\n\n/** Writes made in one attribution scope, by the content ids they wrote (inserts and deletes). */\nexport interface AttributedWrites {\n ids: ContentIds;\n attributions: readonly UpdateAttribution[];\n}\n\n/**\n * The most runs a room keeps; a run is one scope's writes since a write of another scope. Housekeeping\n * all shares one scope, so runs pile up only as Quick Edit steps interleave with it.\n */\nexport const MAX_ATTRIBUTED_RUNS = 64;\n\nfunction noIds(ids: ContentIds): boolean {\n return ids.inserts.isEmpty() && ids.deletes.isEmpty();\n}\n\nfunction sameAttributions(\n a: readonly UpdateAttribution[],\n b: readonly UpdateAttribution[]\n): boolean {\n return (\n a.length === b.length &&\n a.every((pair, index) => pair.k === b[index]?.k && pair.v === b[index]?.v)\n );\n}\n\n/** The sync message `frame` is, when it is exactly one (nothing after its payload). */\nfunction readSyncFrame(frame: Uint8Array): { syncType: number; payload: Uint8Array } | null {\n const type = readVarUint({ bytes: frame, start: 0 });\n if (type?.value !== MESSAGE_SYNC) return null;\n const syncType = readVarUint({ bytes: frame, start: type.next });\n if (syncType === null) return null;\n const length = readVarUint({ bytes: frame, start: syncType.next });\n if (length === null || length.next + length.value !== frame.length) return null;\n return { syncType: syncType.value, payload: frame.subarray(length.next) };\n}\n\n/** A sync message of `syncType` carrying `payload`, as y-protocols writes one. */\nfunction syncFrame({ syncType, payload }: { syncType: number; payload: Uint8Array }): Uint8Array {\n const head: number[] = [];\n writeVarUint({ out: head, value: MESSAGE_SYNC });\n writeVarUint({ out: head, value: syncType });\n writeVarUint({ out: head, value: payload.length });\n const frame = new Uint8Array(head.length + payload.length);\n frame.set(head, 0);\n frame.set(payload, head.length);\n return frame;\n}\n\n/** The content ids an update carries, or `null` for bytes that are not one. */\nfunction idsOf(update: Uint8Array): ContentIds | null {\n try {\n return createContentIdsFromUpdate(update);\n } catch {\n return null;\n }\n}\n\nfunction copyOf({ ids, attributions }: AttributedWrites): AttributedWrites {\n return { ids, attributions: attributions.map(({ k, v }) => ({ k, v })) };\n}\n\n/** `runs` without the oldest past {@link MAX_ATTRIBUTED_RUNS}. */\nfunction bounded(runs: AttributedWrites[]): AttributedWrites[] {\n return runs.length > MAX_ATTRIBUTED_RUNS ? runs.slice(runs.length - MAX_ATTRIBUTED_RUNS) : runs;\n}\n\n/** What a page reads of a room's tracker, whichever copy of this module made it. */\ninterface KeptWrites {\n readonly pending: readonly AttributedWrites[];\n restore(writes: readonly AttributedWrites[]): void;\n}\n\n/**\n * Each document's tracker in the page, and what a destroyed one still kept for the next tracker made\n * for the same document (a transport rebuilt over a retained document; a local copy read into a\n * document before its room's transport exists). On `globalThis`, not in module scope, for the reason\n * the scopes are (`update-attributions.ts`): the SDK's dist inlines its own copy of this module, and\n * a local copy reading a room through another copy must find the same tracker. Weak: each entry goes\n * with its document.\n */\nconst ROOMS = Symbol.for('@athenaintel/keryx-http/unconfirmed-update-attributions');\n\ninterface Rooms {\n live: WeakMap<object, KeptWrites>;\n left: WeakMap<object, AttributedWrites[]>;\n}\n\ntype RoomsHost = typeof globalThis & {\n [ROOMS]?: Rooms;\n};\n\n// process-local: browser-only, each entry keyed by the document whose writes it holds\nfunction rooms(): Rooms {\n const host = globalThis as RoomsHost;\n return (host[ROOMS] ??= { live: new WeakMap(), left: new WeakMap() });\n}\n\n/**\n * The attributed writes Keryx has not acknowledged for `doc`, oldest first: its room's, or what a\n * destroyed room left for the next. What a room's local copy keeps of them.\n */\nexport function unconfirmedAttributedWritesOf(doc: object): readonly AttributedWrites[] {\n const { live, left } = rooms();\n return live.get(doc)?.pending ?? left.get(doc)?.map(copyOf) ?? [];\n}\n\n/**\n * Hands `doc`'s room attributed writes an earlier session kept (the room's local copy), or keeps\n * them for the next tracker made for it. They are older than anything written since, so they go\n * out first.\n */\nexport function restoreUnconfirmedAttributedWrites({\n doc,\n writes,\n}: {\n doc: object;\n writes: readonly AttributedWrites[];\n}): void {\n if (writes.length === 0) return;\n const { live, left } = rooms();\n const room = live.get(doc);\n if (room) {\n room.restore(writes);\n return;\n }\n left.set(doc, bounded([...writes.map(copyOf), ...(left.get(doc) ?? [])]));\n}\n\n/**\n * The two above, as a page hands them to its local copies (`@athenaintel/local-copy`'s\n * `openLocalCopyStore({ attributedWrites })`, `createLocalCopiesPage({ attributedWrites })`): the\n * copy records a room's kept writes with its edits and restores an ended session's to its room.\n */\nexport const roomAttributedWrites = {\n of: unconfirmedAttributedWritesOf,\n restore: restoreUnconfirmedAttributedWrites,\n};\n\n/**\n * One room's attributed writes Keryx has not acknowledged (see the module comment). Made once per\n * room by its transport, which hands it to every socket it opens (`attributingWebSocket`'s\n * `unconfirmed`) and destroys it with the room; a tracker made for the same document later takes\n * over what it still kept, and what its local copy kept from an earlier session\n * ({@link restoreUnconfirmedAttributedWrites}). Not for a room that only listens: its socket sends\n * nothing to split.\n */\nexport class UnconfirmedUpdateAttributions implements UnconfirmedAttributedWrites, KeptWrites {\n private runs: AttributedWrites[];\n private readonly doc: Doc;\n private readonly attributionsFor: () => readonly UpdateAttribution[] | undefined;\n private readonly isRemoteOrigin: (origin: unknown) => boolean;\n\n constructor({\n doc,\n attributionsFor,\n isRemoteOrigin,\n }: {\n doc: Doc;\n /** The attributions of the update being written now: what the socket's trailer reads. */\n attributionsFor: () => readonly UpdateAttribution[] | undefined;\n /** True for a transaction the transport applied (Keryx's state, a peer's edit). */\n isRemoteOrigin: (origin: unknown) => boolean;\n }) {\n this.doc = doc;\n this.attributionsFor = attributionsFor;\n this.isRemoteOrigin = isRemoteOrigin;\n const { live, left } = rooms();\n this.runs = left.get(doc) ?? [];\n left.delete(doc);\n live.set(doc, this);\n doc.on('update', this.handleUpdate);\n }\n\n private readonly handleUpdate = (update: Uint8Array, origin: unknown): void => {\n if (this.isRemoteOrigin(origin)) return;\n const attributions = this.attributionsFor();\n if (!attributions || attributions.length === 0) return;\n const ids = idsOf(update);\n if (!ids || noIds(ids)) return;\n const last = this.runs.at(-1);\n if (last && sameAttributions(last.attributions, attributions)) {\n last.ids = mergeContentIds([last.ids, ids]);\n return;\n }\n this.runs = bounded([...this.runs, copyOf({ ids, attributions })]);\n };\n\n /** The attributed writes Keryx has not acknowledged, oldest first. */\n get pending(): readonly AttributedWrites[] {\n return this.runs.map(copyOf);\n }\n\n /** Writes an earlier session kept, older than any written here: they go out first. */\n restore(writes: readonly AttributedWrites[]): void {\n this.runs = bounded([...writes.map(copyOf), ...this.runs]);\n }\n\n /**\n * One socket of the room. While the room keeps writes, the socket's sync step 2 (its answer to\n * Keryx's sync step 1) waits for the next frame Keryx sends, its whole document, which drops what\n * Keryx already holds before the answer is split (see the module comment); every frame sent\n * meanwhile waits behind it, in order. Nothing kept, or a socket that cannot hear Keryx: every\n * frame goes out as it is sent.\n */\n connect({\n send,\n hears,\n }: {\n send: (frame: Uint8Array) => void;\n hears: boolean;\n }): AttributedWritesSocket {\n let held: Uint8Array[] | null = null;\n const sendNow = (frame: Uint8Array) => {\n for (const out of this.framesFor(frame)) send(out);\n };\n return {\n received: (frame) => {\n this.received(frame);\n if (held === null) return;\n const waiting = held;\n held = null;\n for (const next of waiting) sendNow(next);\n },\n send: (frame) => {\n if (held !== null) {\n held.push(frame);\n return;\n }\n if (hears && this.runs.length > 0 && readSyncFrame(frame)?.syncType === SYNC_STEP_2) {\n held = [frame];\n return;\n }\n sendNow(frame);\n },\n };\n }\n\n /** Keryx sent `frame`: the ids it carries are Keryx's, kept no longer. */\n received(frame: Uint8Array): void {\n if (this.runs.length === 0) return;\n const message = readSyncFrame(frame);\n if (message === null) return;\n if (message.syncType !== SYNC_STEP_2 && message.syncType !== SYNC_UPDATE) return;\n const held = idsOf(message.payload);\n if (!held) return;\n this.runs = this.runs\n .map(({ ids, attributions }) => ({ ids: excludeContentIds(ids, held), attributions }))\n .filter((run) => !noIds(run.ids));\n }\n\n /**\n * What goes out in place of `frame`: `[frame]` itself, except a sync step 2 carrying kept writes,\n * which goes out behind each of them as its own marked sync update.\n */\n framesFor(frame: Uint8Array): readonly Uint8Array[] {\n if (this.runs.length === 0) return [frame];\n const message = readSyncFrame(frame);\n if (message === null || message.syncType !== SYNC_STEP_2) return [frame];\n const answer = message.payload;\n const all = idsOf(answer);\n if (!all) return [frame];\n const frames: Uint8Array[] = [];\n const marked: ContentIds[] = [];\n for (const run of this.runs) {\n const part = intersectContentIds(run.ids, all);\n if (noIds(part)) continue;\n frames.push(\n withUpdateAttributionsTrailer({\n message: syncFrame({\n syncType: SYNC_UPDATE,\n payload: intersectUpdateWithContentIds(answer, part),\n }),\n attributions: run.attributions,\n })\n );\n marked.push(part);\n }\n if (marked.length === 0) return [frame];\n const rest = excludeContentIds(all, mergeContentIds(marked));\n frames.push(\n syncFrame({ syncType: SYNC_STEP_2, payload: intersectUpdateWithContentIds(answer, rest) })\n );\n return frames;\n }\n\n /** Stops keeping; what is still kept waits for the next tracker made for the document. */\n destroy(): void {\n this.doc.off('update', this.handleUpdate);\n const { live, left } = rooms();\n if (live.get(this.doc) === this) live.delete(this.doc);\n if (this.runs.length > 0) left.set(this.doc, this.runs);\n this.runs = [];\n }\n}\n"],"names":["readVarUint","writeVarUint","createContentIdsFromUpdate","mergeContentIds","excludeContentIds","intersectContentIds","withUpdateAttributionsTrailer","intersectUpdateWithContentIds"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"unconfirmed-update-attributions.cjs","sources":["../../../../keryx-http/src/unconfirmed-update-attributions.ts"],"sourcesContent":["/**\n * The attributed writes a room has not seen Keryx acknowledge, kept so that a write made while the\n * room's socket is closed still reaches Keryx with its attributions.\n *\n * A write made inside `runWithDocUpdateAttributions` leaves as its own sync update followed by the\n * attributions trailer (`attributingWebSocket`), but only if the room's socket is open as it is\n * made. One made while it is closed (offline, reconnecting, before the first connection) reaches\n * Keryx inside the reconnect's sync step 2: the client's answer to Keryx's sync step 1, everything\n * it holds that Keryx lacks. Keryx reads no trailer after a sync step 2, so it would count the\n * write as the person's edit.\n *\n * So the room keeps the content ids of every write made in a scope, with the scope's attributions,\n * until Keryx sends those ids back: the echo of the update that carried them, or the whole document\n * Keryx sends when a socket opens (`received`; `@y/websocket` confirms its own unconfirmed updates\n * the same way). When the socket sends a sync step 2, each kept write's part of it goes out first,\n * oldest first, as its own sync update with its trailer, and the sync step 2 follows without those\n * ids (`framesFor`). The parts are cut from the sync step 2 itself, which already holds only what\n * Keryx lacks past its state vector. One socket keeps the order, so Keryx appends each marked update\n * before the person's sync step 2.\n *\n * A room Keryx holds nothing of (its sync step 1 names no client: a document created on the device,\n * its first push) is born whole: assets are born as one write of their real initial state, signed by\n * their creator, and housekeeping inside a new room is part of that state, not system work on\n * anyone's content. Its sync step 2 goes out whole, as the creator's write. What the room kept stays\n * kept until Keryx echoes the push, like any write: a push lost with the socket goes out whole again\n * if Keryx still holds nothing when the room reconnects, and is split like any reconnect's if\n * someone wrote to the room meanwhile.\n *\n * That answer waits for Keryx's document (`connect`). Keryx sends its sync step 1 and, in the same\n * cork, its whole document, but `@y/websocket` answers the sync step 1 as it arrives. A kept write\n * Keryx already appended (its echo lost with the socket) is still kept then, and the sync step 2\n * holds the client's whole delete set, so its deletes would go out again as a marked update, which\n * Keryx appends: it drops what it holds from the first sync step 2 only. Held until the next frame,\n * the answer is split once Keryx's document has dropped what it holds.\n *\n * What Keryx records (`patches/@y%2Fhub@0.10.1.patch`):\n * - An update's content map attributes every id it carries, inserts and deletes, read from its\n * bytes: an update that cannot integrate yet (an item next to one the person typed offline, the\n * next clocks of the person's own client) is attributed like any other. Peers hold it until the\n * sync step 2 behind it brings what it needs.\n * - An id is attributed by the first stream entry that carries it (`getDoc`'s `excludeContentMap`),\n * so a later copy changes no merged history. The live change notices read each entry's own\n * content map, though (keryx/src/doc-watcher.ts), and the first sync step 2's strip of what Keryx\n * holds reads the known ids when the frame arrives, possibly before the update ahead of it is in\n * the stream (one whose content map the compute pool builds). A copy inside the sync step 2\n * would then be announced as the person's: the sync step 2 never carries a kept write's ids.\n *\n * Bounded: consecutive writes of one scope merge into one run, and past\n * {@link MAX_ATTRIBUTED_RUNS} runs the oldest is let go (its content then reaches Keryx in a sync\n * step 2, as the person's). A room's local copy keeps them across sessions\n * ({@link unconfirmedAttributedWritesOf}, {@link restoreUnconfirmedAttributedWrites}); without one,\n * a write the room kept is forgotten with the page.\n */\nimport {\n type ContentIds,\n createContentIdsFromUpdate,\n decodeStateVector,\n type Doc,\n excludeContentIds,\n intersectContentIds,\n intersectUpdateWithContentIds,\n mergeContentIds,\n} from '@y/y';\nimport {\n type AttributedWritesSocket,\n readVarUint,\n type UnconfirmedAttributedWrites,\n type UpdateAttribution,\n withUpdateAttributionsTrailer,\n writeVarUint,\n} from './update-attributions.js';\n\n/** y-protocols: a sync message, the sync step 1 (a state vector) and the sub-types that carry content. */\nconst MESSAGE_SYNC = 0;\nconst SYNC_STEP_1 = 0;\nconst SYNC_STEP_2 = 1;\nconst SYNC_UPDATE = 2;\n\n/** Writes made in one attribution scope, by the content ids they wrote (inserts and deletes). */\nexport interface AttributedWrites {\n ids: ContentIds;\n attributions: readonly UpdateAttribution[];\n}\n\n/**\n * The most runs a room keeps; a run is one scope's writes since a write of another scope. Housekeeping\n * all shares one scope, so runs pile up only as Quick Edit steps interleave with it.\n */\nexport const MAX_ATTRIBUTED_RUNS = 64;\n\nfunction noIds(ids: ContentIds): boolean {\n return ids.inserts.isEmpty() && ids.deletes.isEmpty();\n}\n\nfunction sameAttributions(\n a: readonly UpdateAttribution[],\n b: readonly UpdateAttribution[]\n): boolean {\n return (\n a.length === b.length &&\n a.every((pair, index) => pair.k === b[index]?.k && pair.v === b[index]?.v)\n );\n}\n\n/** The sync message `frame` is, when it is exactly one (nothing after its payload). */\nfunction readSyncFrame(frame: Uint8Array): { syncType: number; payload: Uint8Array } | null {\n const type = readVarUint({ bytes: frame, start: 0 });\n if (type?.value !== MESSAGE_SYNC) return null;\n const syncType = readVarUint({ bytes: frame, start: type.next });\n if (syncType === null) return null;\n const length = readVarUint({ bytes: frame, start: syncType.next });\n if (length === null || length.next + length.value !== frame.length) return null;\n return { syncType: syncType.value, payload: frame.subarray(length.next) };\n}\n\n/** A sync message of `syncType` carrying `payload`, as y-protocols writes one. */\nfunction syncFrame({ syncType, payload }: { syncType: number; payload: Uint8Array }): Uint8Array {\n const head: number[] = [];\n writeVarUint({ out: head, value: MESSAGE_SYNC });\n writeVarUint({ out: head, value: syncType });\n writeVarUint({ out: head, value: payload.length });\n const frame = new Uint8Array(head.length + payload.length);\n frame.set(head, 0);\n frame.set(payload, head.length);\n return frame;\n}\n\n/** Whether `stateVector` (a sync step 1's) names no client: Keryx holds nothing of the room. */\nfunction namesNoClient(stateVector: Uint8Array): boolean {\n try {\n return decodeStateVector(stateVector).size === 0;\n } catch {\n return false;\n }\n}\n\n/** The content ids an update carries, or `null` for bytes that are not one. */\nfunction idsOf(update: Uint8Array): ContentIds | null {\n try {\n return createContentIdsFromUpdate(update);\n } catch {\n return null;\n }\n}\n\nfunction copyOf({ ids, attributions }: AttributedWrites): AttributedWrites {\n return { ids, attributions: attributions.map(({ k, v }) => ({ k, v })) };\n}\n\n/** `runs` without the oldest past {@link MAX_ATTRIBUTED_RUNS}. */\nfunction bounded(runs: AttributedWrites[]): AttributedWrites[] {\n return runs.length > MAX_ATTRIBUTED_RUNS ? runs.slice(runs.length - MAX_ATTRIBUTED_RUNS) : runs;\n}\n\n/** What a page reads of a room's tracker, whichever copy of this module made it. */\ninterface KeptWrites {\n readonly pending: readonly AttributedWrites[];\n restore(writes: readonly AttributedWrites[]): void;\n}\n\n/**\n * Each document's tracker in the page, and what a destroyed one still kept for the next tracker made\n * for the same document (a transport rebuilt over a retained document; a local copy read into a\n * document before its room's transport exists). On `globalThis`, not in module scope, for the reason\n * the scopes are (`update-attributions.ts`): the SDK's dist inlines its own copy of this module, and\n * a local copy reading a room through another copy must find the same tracker. Weak: each entry goes\n * with its document.\n */\nconst ROOMS = Symbol.for('@athenaintel/keryx-http/unconfirmed-update-attributions');\n\ninterface Rooms {\n live: WeakMap<object, KeptWrites>;\n left: WeakMap<object, AttributedWrites[]>;\n}\n\ntype RoomsHost = typeof globalThis & {\n [ROOMS]?: Rooms;\n};\n\n// process-local: browser-only, each entry keyed by the document whose writes it holds\nfunction rooms(): Rooms {\n const host = globalThis as RoomsHost;\n return (host[ROOMS] ??= { live: new WeakMap(), left: new WeakMap() });\n}\n\n/**\n * The attributed writes Keryx has not acknowledged for `doc`, oldest first: its room's, or what a\n * destroyed room left for the next. What a room's local copy keeps of them.\n */\nexport function unconfirmedAttributedWritesOf(doc: object): readonly AttributedWrites[] {\n const { live, left } = rooms();\n return live.get(doc)?.pending ?? left.get(doc)?.map(copyOf) ?? [];\n}\n\n/**\n * Hands `doc`'s room attributed writes an earlier session kept (the room's local copy), or keeps\n * them for the next tracker made for it. They are older than anything written since, so they go\n * out first.\n */\nexport function restoreUnconfirmedAttributedWrites({\n doc,\n writes,\n}: {\n doc: object;\n writes: readonly AttributedWrites[];\n}): void {\n if (writes.length === 0) return;\n const { live, left } = rooms();\n const room = live.get(doc);\n if (room) {\n room.restore(writes);\n return;\n }\n left.set(doc, bounded([...writes.map(copyOf), ...(left.get(doc) ?? [])]));\n}\n\n/**\n * The two above, as a page hands them to its local copies (`@athenaintel/local-copy`'s\n * `openLocalCopyStore({ attributedWrites })`, `createLocalCopiesPage({ attributedWrites })`): the\n * copy records a room's kept writes with its edits and restores an ended session's to its room.\n */\nexport const roomAttributedWrites = {\n of: unconfirmedAttributedWritesOf,\n restore: restoreUnconfirmedAttributedWrites,\n};\n\n/**\n * One room's attributed writes Keryx has not acknowledged (see the module comment). Made once per\n * room by its transport, which hands it to every socket it opens (`attributingWebSocket`'s\n * `unconfirmed`) and destroys it with the room; a tracker made for the same document later takes\n * over what it still kept, and what its local copy kept from an earlier session\n * ({@link restoreUnconfirmedAttributedWrites}). Not for a room that only listens: its socket sends\n * nothing to split.\n */\nexport class UnconfirmedUpdateAttributions implements UnconfirmedAttributedWrites, KeptWrites {\n private runs: AttributedWrites[];\n private readonly doc: Doc;\n private readonly attributionsFor: () => readonly UpdateAttribution[] | undefined;\n private readonly isRemoteOrigin: (origin: unknown) => boolean;\n\n constructor({\n doc,\n attributionsFor,\n isRemoteOrigin,\n }: {\n doc: Doc;\n /** The attributions of the update being written now: what the socket's trailer reads. */\n attributionsFor: () => readonly UpdateAttribution[] | undefined;\n /** True for a transaction the transport applied (Keryx's state, a peer's edit). */\n isRemoteOrigin: (origin: unknown) => boolean;\n }) {\n this.doc = doc;\n this.attributionsFor = attributionsFor;\n this.isRemoteOrigin = isRemoteOrigin;\n const { live, left } = rooms();\n this.runs = left.get(doc) ?? [];\n left.delete(doc);\n live.set(doc, this);\n doc.on('update', this.handleUpdate);\n }\n\n private readonly handleUpdate = (update: Uint8Array, origin: unknown): void => {\n if (this.isRemoteOrigin(origin)) return;\n const attributions = this.attributionsFor();\n if (!attributions || attributions.length === 0) return;\n const ids = idsOf(update);\n if (!ids || noIds(ids)) return;\n const last = this.runs.at(-1);\n if (last && sameAttributions(last.attributions, attributions)) {\n last.ids = mergeContentIds([last.ids, ids]);\n return;\n }\n this.runs = bounded([...this.runs, copyOf({ ids, attributions })]);\n };\n\n /** The attributed writes Keryx has not acknowledged, oldest first. */\n get pending(): readonly AttributedWrites[] {\n return this.runs.map(copyOf);\n }\n\n /** Writes an earlier session kept, older than any written here: they go out first. */\n restore(writes: readonly AttributedWrites[]): void {\n this.runs = bounded([...writes.map(copyOf), ...this.runs]);\n }\n\n /**\n * One socket of the room. While the room keeps writes, the socket's sync step 2 (its answer to\n * Keryx's sync step 1) waits for the next frame Keryx sends, its whole document, which drops what\n * Keryx already holds before the answer is split (see the module comment); every frame sent\n * meanwhile waits behind it, in order. When Keryx's sync step 1 names no client, the room is new\n * to Keryx: its answer goes out whole, born as one write, and what the room kept waits for its\n * echo. Nothing kept, or a socket that cannot hear Keryx: every frame goes out as it is sent.\n */\n connect({\n send,\n hears,\n }: {\n send: (frame: Uint8Array) => void;\n hears: boolean;\n }): AttributedWritesSocket {\n let held: Uint8Array[] | null = null;\n /** Keryx's sync step 1 on this socket named no client: it holds nothing of the room. */\n let roomIsNew = false;\n const sendNow = (frame: Uint8Array) => {\n for (const out of this.framesFor(frame)) send(out);\n };\n return {\n received: (frame) => {\n const message = readSyncFrame(frame);\n if (message?.syncType === SYNC_STEP_1) roomIsNew = namesNoClient(message.payload);\n this.received(frame);\n if (held === null) return;\n const waiting = held;\n held = null;\n for (const next of waiting) sendNow(next);\n },\n send: (frame) => {\n if (held !== null) {\n held.push(frame);\n return;\n }\n const syncType = readSyncFrame(frame)?.syncType;\n if (roomIsNew && syncType === SYNC_STEP_2) {\n send(frame);\n return;\n }\n if (hears && this.runs.length > 0 && syncType === SYNC_STEP_2) {\n held = [frame];\n return;\n }\n sendNow(frame);\n },\n };\n }\n\n /** Keryx sent `frame`: the ids it carries are Keryx's, kept no longer. */\n received(frame: Uint8Array): void {\n if (this.runs.length === 0) return;\n const message = readSyncFrame(frame);\n if (message === null) return;\n if (message.syncType !== SYNC_STEP_2 && message.syncType !== SYNC_UPDATE) return;\n const held = idsOf(message.payload);\n if (!held) return;\n this.runs = this.runs\n .map(({ ids, attributions }) => ({ ids: excludeContentIds(ids, held), attributions }))\n .filter((run) => !noIds(run.ids));\n }\n\n /**\n * What goes out in place of `frame`: `[frame]` itself, except a sync step 2 carrying kept writes,\n * which goes out behind each of them as its own marked sync update.\n */\n framesFor(frame: Uint8Array): readonly Uint8Array[] {\n if (this.runs.length === 0) return [frame];\n const message = readSyncFrame(frame);\n if (message === null || message.syncType !== SYNC_STEP_2) return [frame];\n const answer = message.payload;\n const all = idsOf(answer);\n if (!all) return [frame];\n const frames: Uint8Array[] = [];\n const marked: ContentIds[] = [];\n for (const run of this.runs) {\n const part = intersectContentIds(run.ids, all);\n if (noIds(part)) continue;\n frames.push(\n withUpdateAttributionsTrailer({\n message: syncFrame({\n syncType: SYNC_UPDATE,\n payload: intersectUpdateWithContentIds(answer, part),\n }),\n attributions: run.attributions,\n })\n );\n marked.push(part);\n }\n if (marked.length === 0) return [frame];\n const rest = excludeContentIds(all, mergeContentIds(marked));\n frames.push(\n syncFrame({ syncType: SYNC_STEP_2, payload: intersectUpdateWithContentIds(answer, rest) })\n );\n return frames;\n }\n\n /** Stops keeping; what is still kept waits for the next tracker made for the document. */\n destroy(): void {\n this.doc.off('update', this.handleUpdate);\n const { live, left } = rooms();\n if (live.get(this.doc) === this) live.delete(this.doc);\n if (this.runs.length > 0) left.set(this.doc, this.runs);\n this.runs = [];\n }\n}\n"],"names":["readVarUint","writeVarUint","decodeStateVector","createContentIdsFromUpdate","mergeContentIds","excludeContentIds","intersectContentIds","withUpdateAttributionsTrailer","intersectUpdateWithContentIds"],"mappings":"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"}
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
* Keryx lacks past its state vector. One socket keeps the order, so Keryx appends each marked update
|
|
19
19
|
* before the person's sync step 2.
|
|
20
20
|
*
|
|
21
|
+
* A room Keryx holds nothing of (its sync step 1 names no client: a document created on the device,
|
|
22
|
+
* its first push) is born whole: assets are born as one write of their real initial state, signed by
|
|
23
|
+
* their creator, and housekeeping inside a new room is part of that state, not system work on
|
|
24
|
+
* anyone's content. Its sync step 2 goes out whole, as the creator's write. What the room kept stays
|
|
25
|
+
* kept until Keryx echoes the push, like any write: a push lost with the socket goes out whole again
|
|
26
|
+
* if Keryx still holds nothing when the room reconnects, and is split like any reconnect's if
|
|
27
|
+
* someone wrote to the room meanwhile.
|
|
28
|
+
*
|
|
21
29
|
* That answer waits for Keryx's document (`connect`). Keryx sends its sync step 1 and, in the same
|
|
22
30
|
* cork, its whole document, but `@y/websocket` answers the sync step 1 as it arrives. A kept write
|
|
23
31
|
* Keryx already appended (its echo lost with the socket) is still kept then, and the sync step 2
|
|
@@ -112,8 +120,9 @@ export declare class UnconfirmedUpdateAttributions implements UnconfirmedAttribu
|
|
|
112
120
|
* One socket of the room. While the room keeps writes, the socket's sync step 2 (its answer to
|
|
113
121
|
* Keryx's sync step 1) waits for the next frame Keryx sends, its whole document, which drops what
|
|
114
122
|
* Keryx already holds before the answer is split (see the module comment); every frame sent
|
|
115
|
-
* meanwhile waits behind it, in order.
|
|
116
|
-
*
|
|
123
|
+
* meanwhile waits behind it, in order. When Keryx's sync step 1 names no client, the room is new
|
|
124
|
+
* to Keryx: its answer goes out whole, born as one write, and what the room kept waits for its
|
|
125
|
+
* echo. Nothing kept, or a socket that cannot hear Keryx: every frame goes out as it is sent.
|
|
117
126
|
*/
|
|
118
127
|
connect({ send, hears, }: {
|
|
119
128
|
send: (frame: Uint8Array) => void;
|