@athenaintel/react 0.36.0 → 0.36.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 CHANGED
@@ -774,6 +774,10 @@ Both use the same bearer / API key as session creation. The full recipe
774
774
  template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
775
775
  exported if you hold the bridge state yourself.
776
776
 
777
+ ## What's new in 0.36.1
778
+
779
+ **Opening a long session no longer jumps the transcript around.** As older history pages in above what you are reading, the reading position now stays put, instantly. Before, the chat added its own scroll correction on top of the browser's scroll anchoring (a jump by a page's height), let it play through the viewport's smooth scrolling (a fast scroll through the transcript), and missed a page that folds into the first loaded turn, which moved the text under the reader. Cut tool outputs are now read only for messages near the viewport, at most two at a time, instead of every loaded step at once: opening a long agent run sent dozens of reads, each re-rendering the transcript and all queued ahead of your next message. Nothing to change in your app.
780
+
777
781
  ## What's new in 0.36.0
778
782
 
779
783
  **An agent's modes in your chat.** Pass `modes` (mode ids, `base` for the agent as configured) beside `agent` and `channel`, and `AthenaChat` shows a mode menu in its composer with the ones the agent's published definition allows on that channel: its start mode, the channel itself, and the modes its author lists for it. A pick applies to the open session's next send, or starts a new session in it. It is sent as `collab_channel_id` with the channel as `collab_entry_channel_id`, so the session keeps the channel's switch rules and still shows in a thread list filtered by channel (Athena's server reads that key from this release on and ignores it before). A channel whose author locks it lets people pick a session's mode before its first message only. `useAthenaChatMode()` and `ChatModePicker` build a menu of your own. See [Switching an agent's modes](#switching-an-agents-modes).
@@ -52,6 +52,7 @@ function useDeepAgentWindowSync({
52
52
  nearTop = false,
53
53
  nearTopEpoch = 0,
54
54
  isMessageMounted = everyMessageMounted,
55
+ maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,
55
56
  maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,
56
57
  maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,
57
58
  onHistoryLoad,
@@ -71,6 +72,9 @@ function useDeepAgentWindowSync({
71
72
  const statedRef = react.useRef(null);
72
73
  const restatedConnectionRef = react.useRef(null);
73
74
  const answeredRef = react.useRef(/* @__PURE__ */ new Map());
75
+ const toolOutputsInFlightRef = react.useRef(0);
76
+ const toolOutputsWaitingRef = react.useRef(false);
77
+ const [toolOutputSettles, setToolOutputSettles] = react.useState(0);
74
78
  const onHistoryLoadRef = react.useRef(onHistoryLoad);
75
79
  onHistoryLoadRef.current = onHistoryLoad;
76
80
  const onErrorRef = react.useRef(onError);
@@ -164,6 +168,7 @@ function useDeepAgentWindowSync({
164
168
  react.useEffect(() => {
165
169
  var _a;
166
170
  if (window === null || toolOutputs === void 0) return;
171
+ toolOutputsWaitingRef.current = false;
167
172
  for (const message of messages) {
168
173
  if (!isMessageMounted(message.id)) continue;
169
174
  for (const part of message.content) {
@@ -172,7 +177,12 @@ function useDeepAgentWindowSync({
172
177
  if (marker === null || toolOutputs.isPending(marker.messageId)) continue;
173
178
  if (((_a = toolOutputs.entry(marker.messageId)) == null ? void 0 : _a.chars) === marker.chars) continue;
174
179
  if (answeredRef.current.get(marker.messageId) === marker.chars) continue;
180
+ if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {
181
+ toolOutputsWaitingRef.current = true;
182
+ return;
183
+ }
175
184
  const { messageId, chars } = marker;
185
+ toolOutputsInFlightRef.current += 1;
176
186
  toolOutputs.markPending(messageId);
177
187
  Promise.resolve(sendCommand(messageWindow.buildToolOutputCommand({ messageId }))).then((payload) => {
178
188
  answeredRef.current.set(messageId, chars);
@@ -187,10 +197,23 @@ function useDeepAgentWindowSync({
187
197
  answeredRef.current.set(messageId, chars);
188
198
  toolOutputs.clearPending(messageId);
189
199
  (_a2 = onErrorRef.current) == null ? void 0 : _a2.call(onErrorRef, error, "history/tool_output");
200
+ }).finally(() => {
201
+ toolOutputsInFlightRef.current -= 1;
202
+ if (toolOutputsWaitingRef.current && aliveRef.current) {
203
+ setToolOutputSettles((count) => count + 1);
204
+ }
190
205
  });
191
206
  }
192
207
  }
193
- }, [messages, isMessageMounted, window, sendCommand, toolOutputs]);
208
+ }, [
209
+ messages,
210
+ isMessageMounted,
211
+ window,
212
+ sendCommand,
213
+ toolOutputs,
214
+ maxConcurrentToolOutputs,
215
+ toolOutputSettles
216
+ ]);
194
217
  const loadOlder = react.useCallback(() => {
195
218
  setRequests((count) => count + 1);
196
219
  }, []);
@@ -1 +1 @@
1
- {"version":3,"file":"use-deep-agent-window-sync.cjs","sources":["../../../../agent-runtime/src/use-deep-agent-window-sync.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useAuiState } from '@assistant-ui/store';\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n buildToolOutputCommand,\n type DeepAgentInterestRequest,\n type DeepAgentMessageWindow,\n interestsCovered,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n readToolOutputResult,\n readTruncatedToolOutputArtifact,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\n\n/**\n * Pages of older history chained per approach to the top. A renderer lowers\n * its near-top level once prepended content has pushed the first message out\n * of its band; this bounds the case where it cannot (no viewport to scroll,\n * a list too short to overflow) so a 2,000-message thread never streams whole.\n */\nexport const DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS = 3;\n\n/**\n * Pages of older history that add no converted message — every message on\n * them folds into the first loaded turn, such as one agent run's hundreds of\n * tool steps — loaded on top of a request (or an approach's chain) before it\n * stops. Such a page changes nothing the user can see, so it neither answers\n * an ask nor counts against the per-approach chain.\n */\nexport const DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS = 25;\n\n/** The converted thread messages the tool-output half scans (web and native `ThreadMessage` both satisfy it). */\nexport interface DeepAgentWindowSyncMessage {\n readonly id: string;\n /** The converted role; lets the opening fill tell a turn's prompt from its reply. */\n readonly role?: string;\n readonly content: readonly { readonly type: string; readonly artifact?: unknown }[];\n}\n\n/** One page interest round trip, for the host's telemetry. */\nexport interface DeepAgentHistoryLoadEvent {\n /** The page's ordinal (`1` extends the head). */\n page: number;\n request: DeepAgentInterestRequest;\n durationMs: number;\n}\n\nexport interface UseDeepAgentWindowSyncOptions {\n /**\n * The converted thread messages — read by the host through its platform\n * `useAuiState((s) => s.thread.messages)`, so this hook imports neither\n * `@assistant-ui/react` nor `@assistant-ui/react-native`.\n */\n messages: readonly DeepAgentWindowSyncMessage[];\n /** Messages per page; the thread's window size by default. */\n pageSize?: number;\n /**\n * Full tool outputs fetched back for cut tool messages; the same store the\n * thread converts with (`useDeepAgentThread({ toolOutputs })`). Omitted, the\n * tool-output half is inert (a surface that never shows a cut output whole).\n */\n toolOutputs?: DeepAgentToolOutputStore | undefined;\n /**\n * The renderer's report that the top of the LOADED history is near — a\n * level, not an edge: while it holds and older messages exist, pages load\n * one after another (bounded by `maxChainedLoads` per approach). Omitted for\n * hosts with only a manual affordance (`loadOlder`).\n */\n nearTop?: boolean;\n /** Bumped on every rise of `nearTop` (and every explicit request), so the chain bound is per approach. */\n nearTopEpoch?: number;\n /**\n * Whether a message's content is mounted right now, so lazy fetches (cut\n * tool outputs) target what the user can see rather than the whole loaded\n * history. Omitted: every message counts as mounted (the flat renderers).\n */\n isMessageMounted?: ((messageId: string) => boolean) | undefined;\n maxChainedLoads?: number;\n /** Pages adding no converted message loaded per request before it stops; see {@link DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS}. */\n maxUnseenLoads?: number;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: ((event: DeepAgentHistoryLoadEvent) => void) | undefined;\n /** A read failed; the sync itself recovers (the pending marks are released). */\n onError?: ((error: unknown, command: 'athena/interest' | 'history/tool_output') => void) | undefined;\n now?: () => number;\n}\n\nexport interface DeepAgentWindowSyncState {\n /** The joined window, or `null` while the head interest has not resolved (both halves are inert). */\n window: DeepAgentMessageWindow | null;\n /** Whether the host holds messages older than the loaded window. */\n hasOlder: boolean;\n /**\n * Whether the next page can be asked for: older messages exist and the\n * host's interest cap has room for them. `hasOlder && !canLoadOlder` is the\n * depth limit (`MAX_INTERESTS × MAX_MESSAGE_WINDOW` messages loaded in one\n * connection) — an edge row says so instead of offering a dead button.\n */\n canLoadOlder: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n /** Ask for the next page of older history now (an edge row's button, a keypress). A no-op before the head resolved or at the beginning. */\n loadOlder: () => void;\n}\n\nconst everyMessageMounted = (): boolean => true;\nconst defaultNow = (): number => Date.now();\nconst noopSubscribe = () => () => {};\n\n/** The statewire extras of the nearest aui thread; throws on a non-statewire thread like the library's own hooks. */\nconst useStatewireExtras = (): StatewireThread.Extras =>\n useAuiState((s) => {\n if (!hasStatewireThreadExtras(s.thread.extras)) {\n throw new Error('useDeepAgentWindowSync requires a statewire thread');\n }\n return s.thread.extras as StatewireThread.Extras;\n });\n\n/** The joined interest window of the nearest statewire thread. */\nexport function useDeepAgentMessageWindow(): DeepAgentMessageWindow | null {\n const documents = useStatewireExtras().documents;\n const mounted = useSyncExternalStore(\n documents?.subscribe ?? noopSubscribe,\n () => documents?.list(),\n () => documents?.list()\n );\n return useMemo(() => readDeepAgentMessageWindow(mounted), [mounted]);\n}\n\n/**\n * The client half of the interest window, shared by every surface — mounted\n * once per runtime, inside its aui provider.\n *\n * - **Older history.** While the host renderer reports the top of the loaded\n * history near (`nearTop`) and the window says older messages exist, this\n * re-states the interests with one more page (anchored at the window's\n * first message) — the host mounts its documents for this client alone —\n * chaining at most `maxChainedLoads` pages per approach; `loadOlder` asks\n * for one page on demand (an edge row's button). Never two loads in flight.\n * A reconnect whose hello does not carry the interests this mount last\n * stated (the host remembers a client id's view across its attaches; a new\n * host instance does not) re-states them.\n * - **Cut tool outputs.** Tool cards whose output the wire carried cut\n * (`artifact._athena_content_truncated`) get it fetched whole through\n * `history/tool_output` once their message is mounted; the converter puts\n * it back (`toolOutputs`), so the card re-renders complete. An output the\n * host replaced since (the marker's `chars` changed) is fetched again.\n */\n/**\n * Whether an older page put a new converted message ahead of the thread's\n * first one. Older history only ever merges into or lands before the first\n * converted message, so the second one is a stable landmark: it moved down iff\n * the page showed something. A new tail message does not move it. Without a\n * second message (or once it is gone) the converted count decides.\n */\nfunction pageShowedEarlierMessage(\n from: { count: number; secondId: string | undefined },\n messages: readonly DeepAgentWindowSyncMessage[]\n): boolean {\n if (from.secondId !== undefined) {\n const index = messages.findIndex((message) => message.id === from.secondId);\n if (index !== -1) return index > 1;\n }\n return messages.length > from.count;\n}\n\n/**\n * Whether the loaded thread still opens without context: it should open on a\n * prompt and hold at least one earlier turn, so the newest turn shows with the\n * one before it (a turn is a prompt followed by its reply). Queued and live\n * prompts land at the tail with no reply yet, so they never count; prompts\n * alone need nothing. Without roles, a lone converted message stands in for it.\n */\nfunction opensMidTurn(messages: readonly DeepAgentWindowSyncMessage[]): boolean {\n const first = messages[0];\n if (first === undefined) return false;\n if (messages.every((message) => message.role === undefined)) return messages.length < 2;\n if (first.role !== 'user') return true;\n if (messages.every((message) => message.role === 'user')) return false;\n let turns = 0;\n for (let index = 1; index < messages.length; index += 1) {\n if (messages[index - 1]!.role === 'user' && messages[index]!.role !== 'user') turns += 1;\n }\n return turns < 2;\n}\n\nexport function useDeepAgentWindowSync({\n messages,\n pageSize,\n toolOutputs,\n nearTop = false,\n nearTopEpoch = 0,\n isMessageMounted = everyMessageMounted,\n maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,\n maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,\n onHistoryLoad,\n onError,\n now = defaultNow,\n}: UseDeepAgentWindowSyncOptions): DeepAgentWindowSyncState {\n const { sendCommand, invoke, connection } = useStatewireExtras();\n const window = useDeepAgentMessageWindow();\n const [loading, setLoading] = useState(false);\n const [requests, setRequests] = useState(0);\n const servedRequestsRef = useRef(0);\n const inflightRef = useRef(false);\n const chainRef = useRef({ epoch: 0, loads: 0, unseen: 0, requested: false });\n // Pages loaded to show the opening turn's prompt; `null` once it shows, the\n // user took over, or a page failed — never re-armed for this mount.\n const openingFillRef = useRef<number | null>(0);\n // The converted thread when the in-flight page was stated; `null` once its\n // settle was judged (or it failed).\n const pageFromRef = useRef<{ count: number; secondId: string | undefined } | null>(null);\n const aliveRef = useRef(true);\n // The interests this mount last stated, re-stated on the next connected\n // session (a hello resets the documents) so a reconnect keeps the loaded history.\n const statedRef = useRef<DeepAgentInterestRequest[] | null>(null);\n const restatedConnectionRef = useRef<unknown>(null);\n // Outputs already asked for and answered by this mount — fetched, refused,\n // or failed — by the `chars` they were asked for. The converter re-runs on\n // every wire change, and the store is a bounded LRU: without this memory a\n // refused read would leave on every pass, and once a transcript holds more\n // cut outputs than the store retains, each fetch would evict a still-loaded\n // output and the next pass would fetch it again, forever. An evicted output\n // renders its cut text (the REST display limit) rather than re-entering the\n // loop; a marker whose `chars` moved (the host replaced the output) is asked\n // for again.\n const answeredRef = useRef(new Map<string, number>());\n const onHistoryLoadRef = useRef(onHistoryLoad);\n onHistoryLoadRef.current = onHistoryLoad;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const nowRef = useRef(now);\n nowRef.current = now;\n useEffect(() => {\n aliveRef.current = true;\n return () => {\n aliveRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (connection.status !== 'connected' || restatedConnectionRef.current === connection) return;\n // Decided once per connected session, on the hello's interest document.\n if (window === null) return;\n restatedConnectionRef.current = connection;\n const stated = statedRef.current;\n if (stated === null || interestsCovered(window.requests, stated)) return;\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [stated]).catch((error: unknown) => {\n onErrorRef.current?.(error, 'athena/interest');\n });\n }, [connection, invoke, window]);\n\n useEffect(() => {\n if (window === null || inflightRef.current) return;\n const from = pageFromRef.current;\n pageFromRef.current = null;\n const lastPageUnseen =\n from !== null && from.count > 0 && !pageShowedEarlierMessage(from, messages);\n const pageRequests = pageInterestRequests(window, pageSize);\n if (pageRequests === null) return;\n const requested = requests !== servedRequestsRef.current;\n const chain = chainRef.current;\n let fillPage = false;\n if (\n openingFillRef.current !== null &&\n (requested || nearTop || (messages.length > 0 && !opensMidTurn(messages)))\n ) {\n openingFillRef.current = null;\n }\n if (requested) {\n // An explicit ask re-arms the chain for this approach.\n servedRequestsRef.current = requests;\n chainRef.current = { epoch: nearTopEpoch, loads: 1, unseen: 0, requested: true };\n } else if (lastPageUnseen && (chain.requested || (nearTop && chain.epoch === nearTopEpoch))) {\n // The page landed inside the first loaded turn: keep going until one shows.\n if (chain.unseen >= maxUnseenLoads) return;\n chain.unseen += 1;\n } else if (openingFillRef.current !== null && opensMidTurn(messages)) {\n if (openingFillRef.current >= maxUnseenLoads) return;\n openingFillRef.current += 1;\n fillPage = true;\n } else {\n if (!nearTop) return;\n if (chain.epoch !== nearTopEpoch) {\n chainRef.current = { epoch: nearTopEpoch, loads: 0, unseen: 0, requested: false };\n }\n if (chainRef.current.loads >= maxChainedLoads) return;\n chainRef.current.loads += 1;\n }\n pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\n onHistoryLoadRef.current?.({\n page,\n request,\n durationMs: Math.max(0, Math.round(nowRef.current() - startedAt)),\n });\n })\n .catch((error: unknown) => {\n pageFromRef.current = null;\n if (fillPage) openingFillRef.current = null;\n onErrorRef.current?.(error, 'athena/interest');\n })\n .finally(() => {\n inflightRef.current = false;\n if (aliveRef.current) setLoading(false);\n });\n // `loading` re-arms the chain once a page settled after its documents moved the window.\n }, [\n nearTop,\n nearTopEpoch,\n requests,\n window,\n loading,\n messages,\n invoke,\n maxChainedLoads,\n maxUnseenLoads,\n pageSize,\n ]);\n\n useEffect(() => {\n if (window === null || toolOutputs === undefined) return;\n for (const message of messages) {\n if (!isMessageMounted(message.id)) continue;\n for (const part of message.content) {\n if (part.type !== 'tool-call') continue;\n const marker = readTruncatedToolOutputArtifact(part.artifact);\n if (marker === null || toolOutputs.isPending(marker.messageId)) continue;\n // Fetched for this very length already; a different length means the\n // host replaced the output under the same id since.\n if (toolOutputs.entry(marker.messageId)?.chars === marker.chars) continue;\n if (answeredRef.current.get(marker.messageId) === marker.chars) continue;\n const { messageId, chars } = marker;\n toolOutputs.markPending(messageId);\n Promise.resolve(sendCommand(buildToolOutputCommand({ messageId })))\n .then((payload) => {\n answeredRef.current.set(messageId, chars);\n const result = readToolOutputResult(payload);\n if (result === null) {\n toolOutputs.clearPending(messageId);\n return;\n }\n toolOutputs.set(result.messageId, { content: result.content, chars });\n })\n .catch((error: unknown) => {\n answeredRef.current.set(messageId, chars);\n toolOutputs.clearPending(messageId);\n onErrorRef.current?.(error, 'history/tool_output');\n });\n }\n }\n }, [messages, isMessageMounted, window, sendCommand, toolOutputs]);\n\n const loadOlder = useCallback(() => {\n setRequests((count) => count + 1);\n }, []);\n\n const canLoadOlder = useMemo(\n () => window !== null && pageInterestRequests(window, pageSize) !== null,\n [window, pageSize]\n );\n\n return { window, hasOlder: window?.hasOlder ?? false, canLoadOlder, loading, loadOlder };\n}\n"],"names":["useAuiState","hasStatewireThreadExtras","useSyncExternalStore","useMemo","readDeepAgentMessageWindow","useState","useRef","useEffect","interestsCovered","DEEP_AGENT_INTEREST_PROTOCOL","pageInterestRequests","_a","readTruncatedToolOutputArtifact","buildToolOutputCommand","readToolOutputResult","useCallback"],"mappings":"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"}
1
+ {"version":3,"file":"use-deep-agent-window-sync.cjs","sources":["../../../../agent-runtime/src/use-deep-agent-window-sync.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useAuiState } from '@assistant-ui/store';\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n buildToolOutputCommand,\n type DeepAgentInterestRequest,\n type DeepAgentMessageWindow,\n interestsCovered,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n readToolOutputResult,\n readTruncatedToolOutputArtifact,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\n\n/**\n * Pages of older history chained per approach to the top. A renderer lowers\n * its near-top level once prepended content has pushed the first message out\n * of its band; this bounds the case where it cannot (no viewport to scroll,\n * a list too short to overflow) so a 2,000-message thread never streams whole.\n */\nexport const DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS = 3;\n\n/**\n * Pages of older history that add no converted message — every message on\n * them folds into the first loaded turn, such as one agent run's hundreds of\n * tool steps — loaded on top of a request (or an approach's chain) before it\n * stops. Such a page changes nothing the user can see, so it neither answers\n * an ask nor counts against the per-approach chain.\n */\nexport const DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS = 25;\n\n/** The converted thread messages the tool-output half scans (web and native `ThreadMessage` both satisfy it). */\nexport interface DeepAgentWindowSyncMessage {\n readonly id: string;\n /** The converted role; lets the opening fill tell a turn's prompt from its reply. */\n readonly role?: string;\n readonly content: readonly { readonly type: string; readonly artifact?: unknown }[];\n}\n\n/** One page interest round trip, for the host's telemetry. */\nexport interface DeepAgentHistoryLoadEvent {\n /** The page's ordinal (`1` extends the head). */\n page: number;\n request: DeepAgentInterestRequest;\n durationMs: number;\n}\n\nexport interface UseDeepAgentWindowSyncOptions {\n /**\n * The converted thread messages — read by the host through its platform\n * `useAuiState((s) => s.thread.messages)`, so this hook imports neither\n * `@assistant-ui/react` nor `@assistant-ui/react-native`.\n */\n messages: readonly DeepAgentWindowSyncMessage[];\n /** Messages per page; the thread's window size by default. */\n pageSize?: number;\n /**\n * Full tool outputs fetched back for cut tool messages; the same store the\n * thread converts with (`useDeepAgentThread({ toolOutputs })`). Omitted, the\n * tool-output half is inert (a surface that never shows a cut output whole).\n */\n toolOutputs?: DeepAgentToolOutputStore | undefined;\n /**\n * The renderer's report that the top of the LOADED history is near — a\n * level, not an edge: while it holds and older messages exist, pages load\n * one after another (bounded by `maxChainedLoads` per approach). Omitted for\n * hosts with only a manual affordance (`loadOlder`).\n */\n nearTop?: boolean;\n /** Bumped on every rise of `nearTop` (and every explicit request), so the chain bound is per approach. */\n nearTopEpoch?: number;\n /**\n * Whether a message's content is mounted right now, so lazy fetches (cut\n * tool outputs) target what the user can see rather than the whole loaded\n * history. Omitted: every message counts as mounted (the flat renderers).\n */\n isMessageMounted?: ((messageId: string) => boolean) | undefined;\n /**\n * Cut tool outputs this mount reads at once; the rest wait for one to\n * settle. One mounted message can hold a whole agent run — a hundred tool\n * steps — and the host rate-limits a client's frames, so a reader that sends\n * them all together gets refusals back for most of them. Omitted: every\n * mounted output is asked for together (renderers whose mounted set is\n * already small).\n */\n maxConcurrentToolOutputs?: number;\n maxChainedLoads?: number;\n /** Pages adding no converted message loaded per request before it stops; see {@link DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS}. */\n maxUnseenLoads?: number;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: ((event: DeepAgentHistoryLoadEvent) => void) | undefined;\n /** A read failed; the sync itself recovers (the pending marks are released). */\n onError?: ((error: unknown, command: 'athena/interest' | 'history/tool_output') => void) | undefined;\n now?: () => number;\n}\n\nexport interface DeepAgentWindowSyncState {\n /** The joined window, or `null` while the head interest has not resolved (both halves are inert). */\n window: DeepAgentMessageWindow | null;\n /** Whether the host holds messages older than the loaded window. */\n hasOlder: boolean;\n /**\n * Whether the next page can be asked for: older messages exist and the\n * host's interest cap has room for them. `hasOlder && !canLoadOlder` is the\n * depth limit (`MAX_INTERESTS × MAX_MESSAGE_WINDOW` messages loaded in one\n * connection) — an edge row says so instead of offering a dead button.\n */\n canLoadOlder: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n /** Ask for the next page of older history now (an edge row's button, a keypress). A no-op before the head resolved or at the beginning. */\n loadOlder: () => void;\n}\n\nconst everyMessageMounted = (): boolean => true;\nconst defaultNow = (): number => Date.now();\nconst noopSubscribe = () => () => {};\n\n/** The statewire extras of the nearest aui thread; throws on a non-statewire thread like the library's own hooks. */\nconst useStatewireExtras = (): StatewireThread.Extras =>\n useAuiState((s) => {\n if (!hasStatewireThreadExtras(s.thread.extras)) {\n throw new Error('useDeepAgentWindowSync requires a statewire thread');\n }\n return s.thread.extras as StatewireThread.Extras;\n });\n\n/** The joined interest window of the nearest statewire thread. */\nexport function useDeepAgentMessageWindow(): DeepAgentMessageWindow | null {\n const documents = useStatewireExtras().documents;\n const mounted = useSyncExternalStore(\n documents?.subscribe ?? noopSubscribe,\n () => documents?.list(),\n () => documents?.list()\n );\n return useMemo(() => readDeepAgentMessageWindow(mounted), [mounted]);\n}\n\n/**\n * The client half of the interest window, shared by every surface — mounted\n * once per runtime, inside its aui provider.\n *\n * - **Older history.** While the host renderer reports the top of the loaded\n * history near (`nearTop`) and the window says older messages exist, this\n * re-states the interests with one more page (anchored at the window's\n * first message) — the host mounts its documents for this client alone —\n * chaining at most `maxChainedLoads` pages per approach; `loadOlder` asks\n * for one page on demand (an edge row's button). Never two loads in flight.\n * A reconnect whose hello does not carry the interests this mount last\n * stated (the host remembers a client id's view across its attaches; a new\n * host instance does not) re-states them.\n * - **Cut tool outputs.** Tool cards whose output the wire carried cut\n * (`artifact._athena_content_truncated`) get it fetched whole through\n * `history/tool_output` once their message is mounted, at most\n * `maxConcurrentToolOutputs` at a time; the converter puts it back\n * (`toolOutputs`), so the card re-renders complete. An output the host\n * replaced since (the marker's `chars` changed) is fetched again.\n */\n/**\n * Whether an older page put a new converted message ahead of the thread's\n * first one. Older history only ever merges into or lands before the first\n * converted message, so the second one is a stable landmark: it moved down iff\n * the page showed something. A new tail message does not move it. Without a\n * second message (or once it is gone) the converted count decides.\n */\nfunction pageShowedEarlierMessage(\n from: { count: number; secondId: string | undefined },\n messages: readonly DeepAgentWindowSyncMessage[]\n): boolean {\n if (from.secondId !== undefined) {\n const index = messages.findIndex((message) => message.id === from.secondId);\n if (index !== -1) return index > 1;\n }\n return messages.length > from.count;\n}\n\n/**\n * Whether the loaded thread still opens without context: it should open on a\n * prompt and hold at least one earlier turn, so the newest turn shows with the\n * one before it (a turn is a prompt followed by its reply). Queued and live\n * prompts land at the tail with no reply yet, so they never count; prompts\n * alone need nothing. Without roles, a lone converted message stands in for it.\n */\nfunction opensMidTurn(messages: readonly DeepAgentWindowSyncMessage[]): boolean {\n const first = messages[0];\n if (first === undefined) return false;\n if (messages.every((message) => message.role === undefined)) return messages.length < 2;\n if (first.role !== 'user') return true;\n if (messages.every((message) => message.role === 'user')) return false;\n let turns = 0;\n for (let index = 1; index < messages.length; index += 1) {\n if (messages[index - 1]!.role === 'user' && messages[index]!.role !== 'user') turns += 1;\n }\n return turns < 2;\n}\n\nexport function useDeepAgentWindowSync({\n messages,\n pageSize,\n toolOutputs,\n nearTop = false,\n nearTopEpoch = 0,\n isMessageMounted = everyMessageMounted,\n maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,\n maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,\n maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,\n onHistoryLoad,\n onError,\n now = defaultNow,\n}: UseDeepAgentWindowSyncOptions): DeepAgentWindowSyncState {\n const { sendCommand, invoke, connection } = useStatewireExtras();\n const window = useDeepAgentMessageWindow();\n const [loading, setLoading] = useState(false);\n const [requests, setRequests] = useState(0);\n const servedRequestsRef = useRef(0);\n const inflightRef = useRef(false);\n const chainRef = useRef({ epoch: 0, loads: 0, unseen: 0, requested: false });\n // Pages loaded to show the opening turn's prompt; `null` once it shows, the\n // user took over, or a page failed — never re-armed for this mount.\n const openingFillRef = useRef<number | null>(0);\n // The converted thread when the in-flight page was stated; `null` once its\n // settle was judged (or it failed).\n const pageFromRef = useRef<{ count: number; secondId: string | undefined } | null>(null);\n const aliveRef = useRef(true);\n // The interests this mount last stated, re-stated on the next connected\n // session (a hello resets the documents) so a reconnect keeps the loaded history.\n const statedRef = useRef<DeepAgentInterestRequest[] | null>(null);\n const restatedConnectionRef = useRef<unknown>(null);\n // Outputs already asked for and answered by this mount — fetched, refused,\n // or failed — by the `chars` they were asked for. The converter re-runs on\n // every wire change, and the store is a bounded LRU: without this memory a\n // refused read would leave on every pass, and once a transcript holds more\n // cut outputs than the store retains, each fetch would evict a still-loaded\n // output and the next pass would fetch it again, forever. An evicted output\n // renders its cut text (the REST display limit) rather than re-entering the\n // loop; a marker whose `chars` moved (the host replaced the output) is asked\n // for again.\n const answeredRef = useRef(new Map<string, number>());\n // Reads this mount has in flight, and whether a pass left outputs waiting\n // for one to settle (`maxConcurrentToolOutputs`): only then does a settle\n // re-run the pass, so an unbounded mount renders no more than before.\n const toolOutputsInFlightRef = useRef(0);\n const toolOutputsWaitingRef = useRef(false);\n const [toolOutputSettles, setToolOutputSettles] = useState(0);\n const onHistoryLoadRef = useRef(onHistoryLoad);\n onHistoryLoadRef.current = onHistoryLoad;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const nowRef = useRef(now);\n nowRef.current = now;\n useEffect(() => {\n aliveRef.current = true;\n return () => {\n aliveRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (connection.status !== 'connected' || restatedConnectionRef.current === connection) return;\n // Decided once per connected session, on the hello's interest document.\n if (window === null) return;\n restatedConnectionRef.current = connection;\n const stated = statedRef.current;\n if (stated === null || interestsCovered(window.requests, stated)) return;\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [stated]).catch((error: unknown) => {\n onErrorRef.current?.(error, 'athena/interest');\n });\n }, [connection, invoke, window]);\n\n useEffect(() => {\n if (window === null || inflightRef.current) return;\n const from = pageFromRef.current;\n pageFromRef.current = null;\n const lastPageUnseen =\n from !== null && from.count > 0 && !pageShowedEarlierMessage(from, messages);\n const pageRequests = pageInterestRequests(window, pageSize);\n if (pageRequests === null) return;\n const requested = requests !== servedRequestsRef.current;\n const chain = chainRef.current;\n let fillPage = false;\n if (\n openingFillRef.current !== null &&\n (requested || nearTop || (messages.length > 0 && !opensMidTurn(messages)))\n ) {\n openingFillRef.current = null;\n }\n if (requested) {\n // An explicit ask re-arms the chain for this approach.\n servedRequestsRef.current = requests;\n chainRef.current = { epoch: nearTopEpoch, loads: 1, unseen: 0, requested: true };\n } else if (lastPageUnseen && (chain.requested || (nearTop && chain.epoch === nearTopEpoch))) {\n // The page landed inside the first loaded turn: keep going until one shows.\n if (chain.unseen >= maxUnseenLoads) return;\n chain.unseen += 1;\n } else if (openingFillRef.current !== null && opensMidTurn(messages)) {\n if (openingFillRef.current >= maxUnseenLoads) return;\n openingFillRef.current += 1;\n fillPage = true;\n } else {\n if (!nearTop) return;\n if (chain.epoch !== nearTopEpoch) {\n chainRef.current = { epoch: nearTopEpoch, loads: 0, unseen: 0, requested: false };\n }\n if (chainRef.current.loads >= maxChainedLoads) return;\n chainRef.current.loads += 1;\n }\n pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\n onHistoryLoadRef.current?.({\n page,\n request,\n durationMs: Math.max(0, Math.round(nowRef.current() - startedAt)),\n });\n })\n .catch((error: unknown) => {\n pageFromRef.current = null;\n if (fillPage) openingFillRef.current = null;\n onErrorRef.current?.(error, 'athena/interest');\n })\n .finally(() => {\n inflightRef.current = false;\n if (aliveRef.current) setLoading(false);\n });\n // `loading` re-arms the chain once a page settled after its documents moved the window.\n }, [\n nearTop,\n nearTopEpoch,\n requests,\n window,\n loading,\n messages,\n invoke,\n maxChainedLoads,\n maxUnseenLoads,\n pageSize,\n ]);\n\n useEffect(() => {\n if (window === null || toolOutputs === undefined) return;\n toolOutputsWaitingRef.current = false;\n for (const message of messages) {\n if (!isMessageMounted(message.id)) continue;\n for (const part of message.content) {\n if (part.type !== 'tool-call') continue;\n const marker = readTruncatedToolOutputArtifact(part.artifact);\n if (marker === null || toolOutputs.isPending(marker.messageId)) continue;\n // Fetched for this very length already; a different length means the\n // host replaced the output under the same id since.\n if (toolOutputs.entry(marker.messageId)?.chars === marker.chars) continue;\n if (answeredRef.current.get(marker.messageId) === marker.chars) continue;\n if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {\n toolOutputsWaitingRef.current = true;\n return;\n }\n const { messageId, chars } = marker;\n toolOutputsInFlightRef.current += 1;\n toolOutputs.markPending(messageId);\n Promise.resolve(sendCommand(buildToolOutputCommand({ messageId })))\n .then((payload) => {\n answeredRef.current.set(messageId, chars);\n const result = readToolOutputResult(payload);\n if (result === null) {\n toolOutputs.clearPending(messageId);\n return;\n }\n toolOutputs.set(result.messageId, { content: result.content, chars });\n })\n .catch((error: unknown) => {\n answeredRef.current.set(messageId, chars);\n toolOutputs.clearPending(messageId);\n onErrorRef.current?.(error, 'history/tool_output');\n })\n .finally(() => {\n toolOutputsInFlightRef.current -= 1;\n if (toolOutputsWaitingRef.current && aliveRef.current) {\n setToolOutputSettles((count) => count + 1);\n }\n });\n }\n }\n }, [\n messages,\n isMessageMounted,\n window,\n sendCommand,\n toolOutputs,\n maxConcurrentToolOutputs,\n toolOutputSettles,\n ]);\n\n const loadOlder = useCallback(() => {\n setRequests((count) => count + 1);\n }, []);\n\n const canLoadOlder = useMemo(\n () => window !== null && pageInterestRequests(window, pageSize) !== null,\n [window, pageSize]\n );\n\n return { window, hasOlder: window?.hasOlder ?? false, canLoadOlder, loading, loadOlder };\n}\n"],"names":["useAuiState","hasStatewireThreadExtras","useSyncExternalStore","useMemo","readDeepAgentMessageWindow","useState","useRef","useEffect","interestsCovered","DEEP_AGENT_INTEREST_PROTOCOL","pageInterestRequests","_a","readTruncatedToolOutputArtifact","buildToolOutputCommand","readToolOutputResult","useCallback"],"mappings":"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"}
@@ -50,6 +50,7 @@ function useDeepAgentWindowSync({
50
50
  nearTop = false,
51
51
  nearTopEpoch = 0,
52
52
  isMessageMounted = everyMessageMounted,
53
+ maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,
53
54
  maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,
54
55
  maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,
55
56
  onHistoryLoad,
@@ -69,6 +70,9 @@ function useDeepAgentWindowSync({
69
70
  const statedRef = useRef(null);
70
71
  const restatedConnectionRef = useRef(null);
71
72
  const answeredRef = useRef(/* @__PURE__ */ new Map());
73
+ const toolOutputsInFlightRef = useRef(0);
74
+ const toolOutputsWaitingRef = useRef(false);
75
+ const [toolOutputSettles, setToolOutputSettles] = useState(0);
72
76
  const onHistoryLoadRef = useRef(onHistoryLoad);
73
77
  onHistoryLoadRef.current = onHistoryLoad;
74
78
  const onErrorRef = useRef(onError);
@@ -162,6 +166,7 @@ function useDeepAgentWindowSync({
162
166
  useEffect(() => {
163
167
  var _a;
164
168
  if (window === null || toolOutputs === void 0) return;
169
+ toolOutputsWaitingRef.current = false;
165
170
  for (const message of messages) {
166
171
  if (!isMessageMounted(message.id)) continue;
167
172
  for (const part of message.content) {
@@ -170,7 +175,12 @@ function useDeepAgentWindowSync({
170
175
  if (marker === null || toolOutputs.isPending(marker.messageId)) continue;
171
176
  if (((_a = toolOutputs.entry(marker.messageId)) == null ? void 0 : _a.chars) === marker.chars) continue;
172
177
  if (answeredRef.current.get(marker.messageId) === marker.chars) continue;
178
+ if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {
179
+ toolOutputsWaitingRef.current = true;
180
+ return;
181
+ }
173
182
  const { messageId, chars } = marker;
183
+ toolOutputsInFlightRef.current += 1;
174
184
  toolOutputs.markPending(messageId);
175
185
  Promise.resolve(sendCommand(buildToolOutputCommand({ messageId }))).then((payload) => {
176
186
  answeredRef.current.set(messageId, chars);
@@ -185,10 +195,23 @@ function useDeepAgentWindowSync({
185
195
  answeredRef.current.set(messageId, chars);
186
196
  toolOutputs.clearPending(messageId);
187
197
  (_a2 = onErrorRef.current) == null ? void 0 : _a2.call(onErrorRef, error, "history/tool_output");
198
+ }).finally(() => {
199
+ toolOutputsInFlightRef.current -= 1;
200
+ if (toolOutputsWaitingRef.current && aliveRef.current) {
201
+ setToolOutputSettles((count) => count + 1);
202
+ }
188
203
  });
189
204
  }
190
205
  }
191
- }, [messages, isMessageMounted, window, sendCommand, toolOutputs]);
206
+ }, [
207
+ messages,
208
+ isMessageMounted,
209
+ window,
210
+ sendCommand,
211
+ toolOutputs,
212
+ maxConcurrentToolOutputs,
213
+ toolOutputSettles
214
+ ]);
192
215
  const loadOlder = useCallback(() => {
193
216
  setRequests((count) => count + 1);
194
217
  }, []);
@@ -1 +1 @@
1
- {"version":3,"file":"use-deep-agent-window-sync.js","sources":["../../../../agent-runtime/src/use-deep-agent-window-sync.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useAuiState } from '@assistant-ui/store';\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n buildToolOutputCommand,\n type DeepAgentInterestRequest,\n type DeepAgentMessageWindow,\n interestsCovered,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n readToolOutputResult,\n readTruncatedToolOutputArtifact,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\n\n/**\n * Pages of older history chained per approach to the top. A renderer lowers\n * its near-top level once prepended content has pushed the first message out\n * of its band; this bounds the case where it cannot (no viewport to scroll,\n * a list too short to overflow) so a 2,000-message thread never streams whole.\n */\nexport const DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS = 3;\n\n/**\n * Pages of older history that add no converted message — every message on\n * them folds into the first loaded turn, such as one agent run's hundreds of\n * tool steps — loaded on top of a request (or an approach's chain) before it\n * stops. Such a page changes nothing the user can see, so it neither answers\n * an ask nor counts against the per-approach chain.\n */\nexport const DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS = 25;\n\n/** The converted thread messages the tool-output half scans (web and native `ThreadMessage` both satisfy it). */\nexport interface DeepAgentWindowSyncMessage {\n readonly id: string;\n /** The converted role; lets the opening fill tell a turn's prompt from its reply. */\n readonly role?: string;\n readonly content: readonly { readonly type: string; readonly artifact?: unknown }[];\n}\n\n/** One page interest round trip, for the host's telemetry. */\nexport interface DeepAgentHistoryLoadEvent {\n /** The page's ordinal (`1` extends the head). */\n page: number;\n request: DeepAgentInterestRequest;\n durationMs: number;\n}\n\nexport interface UseDeepAgentWindowSyncOptions {\n /**\n * The converted thread messages — read by the host through its platform\n * `useAuiState((s) => s.thread.messages)`, so this hook imports neither\n * `@assistant-ui/react` nor `@assistant-ui/react-native`.\n */\n messages: readonly DeepAgentWindowSyncMessage[];\n /** Messages per page; the thread's window size by default. */\n pageSize?: number;\n /**\n * Full tool outputs fetched back for cut tool messages; the same store the\n * thread converts with (`useDeepAgentThread({ toolOutputs })`). Omitted, the\n * tool-output half is inert (a surface that never shows a cut output whole).\n */\n toolOutputs?: DeepAgentToolOutputStore | undefined;\n /**\n * The renderer's report that the top of the LOADED history is near — a\n * level, not an edge: while it holds and older messages exist, pages load\n * one after another (bounded by `maxChainedLoads` per approach). Omitted for\n * hosts with only a manual affordance (`loadOlder`).\n */\n nearTop?: boolean;\n /** Bumped on every rise of `nearTop` (and every explicit request), so the chain bound is per approach. */\n nearTopEpoch?: number;\n /**\n * Whether a message's content is mounted right now, so lazy fetches (cut\n * tool outputs) target what the user can see rather than the whole loaded\n * history. Omitted: every message counts as mounted (the flat renderers).\n */\n isMessageMounted?: ((messageId: string) => boolean) | undefined;\n maxChainedLoads?: number;\n /** Pages adding no converted message loaded per request before it stops; see {@link DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS}. */\n maxUnseenLoads?: number;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: ((event: DeepAgentHistoryLoadEvent) => void) | undefined;\n /** A read failed; the sync itself recovers (the pending marks are released). */\n onError?: ((error: unknown, command: 'athena/interest' | 'history/tool_output') => void) | undefined;\n now?: () => number;\n}\n\nexport interface DeepAgentWindowSyncState {\n /** The joined window, or `null` while the head interest has not resolved (both halves are inert). */\n window: DeepAgentMessageWindow | null;\n /** Whether the host holds messages older than the loaded window. */\n hasOlder: boolean;\n /**\n * Whether the next page can be asked for: older messages exist and the\n * host's interest cap has room for them. `hasOlder && !canLoadOlder` is the\n * depth limit (`MAX_INTERESTS × MAX_MESSAGE_WINDOW` messages loaded in one\n * connection) — an edge row says so instead of offering a dead button.\n */\n canLoadOlder: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n /** Ask for the next page of older history now (an edge row's button, a keypress). A no-op before the head resolved or at the beginning. */\n loadOlder: () => void;\n}\n\nconst everyMessageMounted = (): boolean => true;\nconst defaultNow = (): number => Date.now();\nconst noopSubscribe = () => () => {};\n\n/** The statewire extras of the nearest aui thread; throws on a non-statewire thread like the library's own hooks. */\nconst useStatewireExtras = (): StatewireThread.Extras =>\n useAuiState((s) => {\n if (!hasStatewireThreadExtras(s.thread.extras)) {\n throw new Error('useDeepAgentWindowSync requires a statewire thread');\n }\n return s.thread.extras as StatewireThread.Extras;\n });\n\n/** The joined interest window of the nearest statewire thread. */\nexport function useDeepAgentMessageWindow(): DeepAgentMessageWindow | null {\n const documents = useStatewireExtras().documents;\n const mounted = useSyncExternalStore(\n documents?.subscribe ?? noopSubscribe,\n () => documents?.list(),\n () => documents?.list()\n );\n return useMemo(() => readDeepAgentMessageWindow(mounted), [mounted]);\n}\n\n/**\n * The client half of the interest window, shared by every surface — mounted\n * once per runtime, inside its aui provider.\n *\n * - **Older history.** While the host renderer reports the top of the loaded\n * history near (`nearTop`) and the window says older messages exist, this\n * re-states the interests with one more page (anchored at the window's\n * first message) — the host mounts its documents for this client alone —\n * chaining at most `maxChainedLoads` pages per approach; `loadOlder` asks\n * for one page on demand (an edge row's button). Never two loads in flight.\n * A reconnect whose hello does not carry the interests this mount last\n * stated (the host remembers a client id's view across its attaches; a new\n * host instance does not) re-states them.\n * - **Cut tool outputs.** Tool cards whose output the wire carried cut\n * (`artifact._athena_content_truncated`) get it fetched whole through\n * `history/tool_output` once their message is mounted; the converter puts\n * it back (`toolOutputs`), so the card re-renders complete. An output the\n * host replaced since (the marker's `chars` changed) is fetched again.\n */\n/**\n * Whether an older page put a new converted message ahead of the thread's\n * first one. Older history only ever merges into or lands before the first\n * converted message, so the second one is a stable landmark: it moved down iff\n * the page showed something. A new tail message does not move it. Without a\n * second message (or once it is gone) the converted count decides.\n */\nfunction pageShowedEarlierMessage(\n from: { count: number; secondId: string | undefined },\n messages: readonly DeepAgentWindowSyncMessage[]\n): boolean {\n if (from.secondId !== undefined) {\n const index = messages.findIndex((message) => message.id === from.secondId);\n if (index !== -1) return index > 1;\n }\n return messages.length > from.count;\n}\n\n/**\n * Whether the loaded thread still opens without context: it should open on a\n * prompt and hold at least one earlier turn, so the newest turn shows with the\n * one before it (a turn is a prompt followed by its reply). Queued and live\n * prompts land at the tail with no reply yet, so they never count; prompts\n * alone need nothing. Without roles, a lone converted message stands in for it.\n */\nfunction opensMidTurn(messages: readonly DeepAgentWindowSyncMessage[]): boolean {\n const first = messages[0];\n if (first === undefined) return false;\n if (messages.every((message) => message.role === undefined)) return messages.length < 2;\n if (first.role !== 'user') return true;\n if (messages.every((message) => message.role === 'user')) return false;\n let turns = 0;\n for (let index = 1; index < messages.length; index += 1) {\n if (messages[index - 1]!.role === 'user' && messages[index]!.role !== 'user') turns += 1;\n }\n return turns < 2;\n}\n\nexport function useDeepAgentWindowSync({\n messages,\n pageSize,\n toolOutputs,\n nearTop = false,\n nearTopEpoch = 0,\n isMessageMounted = everyMessageMounted,\n maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,\n maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,\n onHistoryLoad,\n onError,\n now = defaultNow,\n}: UseDeepAgentWindowSyncOptions): DeepAgentWindowSyncState {\n const { sendCommand, invoke, connection } = useStatewireExtras();\n const window = useDeepAgentMessageWindow();\n const [loading, setLoading] = useState(false);\n const [requests, setRequests] = useState(0);\n const servedRequestsRef = useRef(0);\n const inflightRef = useRef(false);\n const chainRef = useRef({ epoch: 0, loads: 0, unseen: 0, requested: false });\n // Pages loaded to show the opening turn's prompt; `null` once it shows, the\n // user took over, or a page failed — never re-armed for this mount.\n const openingFillRef = useRef<number | null>(0);\n // The converted thread when the in-flight page was stated; `null` once its\n // settle was judged (or it failed).\n const pageFromRef = useRef<{ count: number; secondId: string | undefined } | null>(null);\n const aliveRef = useRef(true);\n // The interests this mount last stated, re-stated on the next connected\n // session (a hello resets the documents) so a reconnect keeps the loaded history.\n const statedRef = useRef<DeepAgentInterestRequest[] | null>(null);\n const restatedConnectionRef = useRef<unknown>(null);\n // Outputs already asked for and answered by this mount — fetched, refused,\n // or failed — by the `chars` they were asked for. The converter re-runs on\n // every wire change, and the store is a bounded LRU: without this memory a\n // refused read would leave on every pass, and once a transcript holds more\n // cut outputs than the store retains, each fetch would evict a still-loaded\n // output and the next pass would fetch it again, forever. An evicted output\n // renders its cut text (the REST display limit) rather than re-entering the\n // loop; a marker whose `chars` moved (the host replaced the output) is asked\n // for again.\n const answeredRef = useRef(new Map<string, number>());\n const onHistoryLoadRef = useRef(onHistoryLoad);\n onHistoryLoadRef.current = onHistoryLoad;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const nowRef = useRef(now);\n nowRef.current = now;\n useEffect(() => {\n aliveRef.current = true;\n return () => {\n aliveRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (connection.status !== 'connected' || restatedConnectionRef.current === connection) return;\n // Decided once per connected session, on the hello's interest document.\n if (window === null) return;\n restatedConnectionRef.current = connection;\n const stated = statedRef.current;\n if (stated === null || interestsCovered(window.requests, stated)) return;\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [stated]).catch((error: unknown) => {\n onErrorRef.current?.(error, 'athena/interest');\n });\n }, [connection, invoke, window]);\n\n useEffect(() => {\n if (window === null || inflightRef.current) return;\n const from = pageFromRef.current;\n pageFromRef.current = null;\n const lastPageUnseen =\n from !== null && from.count > 0 && !pageShowedEarlierMessage(from, messages);\n const pageRequests = pageInterestRequests(window, pageSize);\n if (pageRequests === null) return;\n const requested = requests !== servedRequestsRef.current;\n const chain = chainRef.current;\n let fillPage = false;\n if (\n openingFillRef.current !== null &&\n (requested || nearTop || (messages.length > 0 && !opensMidTurn(messages)))\n ) {\n openingFillRef.current = null;\n }\n if (requested) {\n // An explicit ask re-arms the chain for this approach.\n servedRequestsRef.current = requests;\n chainRef.current = { epoch: nearTopEpoch, loads: 1, unseen: 0, requested: true };\n } else if (lastPageUnseen && (chain.requested || (nearTop && chain.epoch === nearTopEpoch))) {\n // The page landed inside the first loaded turn: keep going until one shows.\n if (chain.unseen >= maxUnseenLoads) return;\n chain.unseen += 1;\n } else if (openingFillRef.current !== null && opensMidTurn(messages)) {\n if (openingFillRef.current >= maxUnseenLoads) return;\n openingFillRef.current += 1;\n fillPage = true;\n } else {\n if (!nearTop) return;\n if (chain.epoch !== nearTopEpoch) {\n chainRef.current = { epoch: nearTopEpoch, loads: 0, unseen: 0, requested: false };\n }\n if (chainRef.current.loads >= maxChainedLoads) return;\n chainRef.current.loads += 1;\n }\n pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\n onHistoryLoadRef.current?.({\n page,\n request,\n durationMs: Math.max(0, Math.round(nowRef.current() - startedAt)),\n });\n })\n .catch((error: unknown) => {\n pageFromRef.current = null;\n if (fillPage) openingFillRef.current = null;\n onErrorRef.current?.(error, 'athena/interest');\n })\n .finally(() => {\n inflightRef.current = false;\n if (aliveRef.current) setLoading(false);\n });\n // `loading` re-arms the chain once a page settled after its documents moved the window.\n }, [\n nearTop,\n nearTopEpoch,\n requests,\n window,\n loading,\n messages,\n invoke,\n maxChainedLoads,\n maxUnseenLoads,\n pageSize,\n ]);\n\n useEffect(() => {\n if (window === null || toolOutputs === undefined) return;\n for (const message of messages) {\n if (!isMessageMounted(message.id)) continue;\n for (const part of message.content) {\n if (part.type !== 'tool-call') continue;\n const marker = readTruncatedToolOutputArtifact(part.artifact);\n if (marker === null || toolOutputs.isPending(marker.messageId)) continue;\n // Fetched for this very length already; a different length means the\n // host replaced the output under the same id since.\n if (toolOutputs.entry(marker.messageId)?.chars === marker.chars) continue;\n if (answeredRef.current.get(marker.messageId) === marker.chars) continue;\n const { messageId, chars } = marker;\n toolOutputs.markPending(messageId);\n Promise.resolve(sendCommand(buildToolOutputCommand({ messageId })))\n .then((payload) => {\n answeredRef.current.set(messageId, chars);\n const result = readToolOutputResult(payload);\n if (result === null) {\n toolOutputs.clearPending(messageId);\n return;\n }\n toolOutputs.set(result.messageId, { content: result.content, chars });\n })\n .catch((error: unknown) => {\n answeredRef.current.set(messageId, chars);\n toolOutputs.clearPending(messageId);\n onErrorRef.current?.(error, 'history/tool_output');\n });\n }\n }\n }, [messages, isMessageMounted, window, sendCommand, toolOutputs]);\n\n const loadOlder = useCallback(() => {\n setRequests((count) => count + 1);\n }, []);\n\n const canLoadOlder = useMemo(\n () => window !== null && pageInterestRequests(window, pageSize) !== null,\n [window, pageSize]\n );\n\n return { window, hasOlder: window?.hasOlder ?? false, canLoadOlder, loading, loadOlder };\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"use-deep-agent-window-sync.js","sources":["../../../../agent-runtime/src/use-deep-agent-window-sync.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useAuiState } from '@assistant-ui/store';\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n buildToolOutputCommand,\n type DeepAgentInterestRequest,\n type DeepAgentMessageWindow,\n interestsCovered,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n readToolOutputResult,\n readTruncatedToolOutputArtifact,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\n\n/**\n * Pages of older history chained per approach to the top. A renderer lowers\n * its near-top level once prepended content has pushed the first message out\n * of its band; this bounds the case where it cannot (no viewport to scroll,\n * a list too short to overflow) so a 2,000-message thread never streams whole.\n */\nexport const DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS = 3;\n\n/**\n * Pages of older history that add no converted message — every message on\n * them folds into the first loaded turn, such as one agent run's hundreds of\n * tool steps — loaded on top of a request (or an approach's chain) before it\n * stops. Such a page changes nothing the user can see, so it neither answers\n * an ask nor counts against the per-approach chain.\n */\nexport const DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS = 25;\n\n/** The converted thread messages the tool-output half scans (web and native `ThreadMessage` both satisfy it). */\nexport interface DeepAgentWindowSyncMessage {\n readonly id: string;\n /** The converted role; lets the opening fill tell a turn's prompt from its reply. */\n readonly role?: string;\n readonly content: readonly { readonly type: string; readonly artifact?: unknown }[];\n}\n\n/** One page interest round trip, for the host's telemetry. */\nexport interface DeepAgentHistoryLoadEvent {\n /** The page's ordinal (`1` extends the head). */\n page: number;\n request: DeepAgentInterestRequest;\n durationMs: number;\n}\n\nexport interface UseDeepAgentWindowSyncOptions {\n /**\n * The converted thread messages — read by the host through its platform\n * `useAuiState((s) => s.thread.messages)`, so this hook imports neither\n * `@assistant-ui/react` nor `@assistant-ui/react-native`.\n */\n messages: readonly DeepAgentWindowSyncMessage[];\n /** Messages per page; the thread's window size by default. */\n pageSize?: number;\n /**\n * Full tool outputs fetched back for cut tool messages; the same store the\n * thread converts with (`useDeepAgentThread({ toolOutputs })`). Omitted, the\n * tool-output half is inert (a surface that never shows a cut output whole).\n */\n toolOutputs?: DeepAgentToolOutputStore | undefined;\n /**\n * The renderer's report that the top of the LOADED history is near — a\n * level, not an edge: while it holds and older messages exist, pages load\n * one after another (bounded by `maxChainedLoads` per approach). Omitted for\n * hosts with only a manual affordance (`loadOlder`).\n */\n nearTop?: boolean;\n /** Bumped on every rise of `nearTop` (and every explicit request), so the chain bound is per approach. */\n nearTopEpoch?: number;\n /**\n * Whether a message's content is mounted right now, so lazy fetches (cut\n * tool outputs) target what the user can see rather than the whole loaded\n * history. Omitted: every message counts as mounted (the flat renderers).\n */\n isMessageMounted?: ((messageId: string) => boolean) | undefined;\n /**\n * Cut tool outputs this mount reads at once; the rest wait for one to\n * settle. One mounted message can hold a whole agent run — a hundred tool\n * steps — and the host rate-limits a client's frames, so a reader that sends\n * them all together gets refusals back for most of them. Omitted: every\n * mounted output is asked for together (renderers whose mounted set is\n * already small).\n */\n maxConcurrentToolOutputs?: number;\n maxChainedLoads?: number;\n /** Pages adding no converted message loaded per request before it stops; see {@link DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS}. */\n maxUnseenLoads?: number;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: ((event: DeepAgentHistoryLoadEvent) => void) | undefined;\n /** A read failed; the sync itself recovers (the pending marks are released). */\n onError?: ((error: unknown, command: 'athena/interest' | 'history/tool_output') => void) | undefined;\n now?: () => number;\n}\n\nexport interface DeepAgentWindowSyncState {\n /** The joined window, or `null` while the head interest has not resolved (both halves are inert). */\n window: DeepAgentMessageWindow | null;\n /** Whether the host holds messages older than the loaded window. */\n hasOlder: boolean;\n /**\n * Whether the next page can be asked for: older messages exist and the\n * host's interest cap has room for them. `hasOlder && !canLoadOlder` is the\n * depth limit (`MAX_INTERESTS × MAX_MESSAGE_WINDOW` messages loaded in one\n * connection) — an edge row says so instead of offering a dead button.\n */\n canLoadOlder: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n /** Ask for the next page of older history now (an edge row's button, a keypress). A no-op before the head resolved or at the beginning. */\n loadOlder: () => void;\n}\n\nconst everyMessageMounted = (): boolean => true;\nconst defaultNow = (): number => Date.now();\nconst noopSubscribe = () => () => {};\n\n/** The statewire extras of the nearest aui thread; throws on a non-statewire thread like the library's own hooks. */\nconst useStatewireExtras = (): StatewireThread.Extras =>\n useAuiState((s) => {\n if (!hasStatewireThreadExtras(s.thread.extras)) {\n throw new Error('useDeepAgentWindowSync requires a statewire thread');\n }\n return s.thread.extras as StatewireThread.Extras;\n });\n\n/** The joined interest window of the nearest statewire thread. */\nexport function useDeepAgentMessageWindow(): DeepAgentMessageWindow | null {\n const documents = useStatewireExtras().documents;\n const mounted = useSyncExternalStore(\n documents?.subscribe ?? noopSubscribe,\n () => documents?.list(),\n () => documents?.list()\n );\n return useMemo(() => readDeepAgentMessageWindow(mounted), [mounted]);\n}\n\n/**\n * The client half of the interest window, shared by every surface — mounted\n * once per runtime, inside its aui provider.\n *\n * - **Older history.** While the host renderer reports the top of the loaded\n * history near (`nearTop`) and the window says older messages exist, this\n * re-states the interests with one more page (anchored at the window's\n * first message) — the host mounts its documents for this client alone —\n * chaining at most `maxChainedLoads` pages per approach; `loadOlder` asks\n * for one page on demand (an edge row's button). Never two loads in flight.\n * A reconnect whose hello does not carry the interests this mount last\n * stated (the host remembers a client id's view across its attaches; a new\n * host instance does not) re-states them.\n * - **Cut tool outputs.** Tool cards whose output the wire carried cut\n * (`artifact._athena_content_truncated`) get it fetched whole through\n * `history/tool_output` once their message is mounted, at most\n * `maxConcurrentToolOutputs` at a time; the converter puts it back\n * (`toolOutputs`), so the card re-renders complete. An output the host\n * replaced since (the marker's `chars` changed) is fetched again.\n */\n/**\n * Whether an older page put a new converted message ahead of the thread's\n * first one. Older history only ever merges into or lands before the first\n * converted message, so the second one is a stable landmark: it moved down iff\n * the page showed something. A new tail message does not move it. Without a\n * second message (or once it is gone) the converted count decides.\n */\nfunction pageShowedEarlierMessage(\n from: { count: number; secondId: string | undefined },\n messages: readonly DeepAgentWindowSyncMessage[]\n): boolean {\n if (from.secondId !== undefined) {\n const index = messages.findIndex((message) => message.id === from.secondId);\n if (index !== -1) return index > 1;\n }\n return messages.length > from.count;\n}\n\n/**\n * Whether the loaded thread still opens without context: it should open on a\n * prompt and hold at least one earlier turn, so the newest turn shows with the\n * one before it (a turn is a prompt followed by its reply). Queued and live\n * prompts land at the tail with no reply yet, so they never count; prompts\n * alone need nothing. Without roles, a lone converted message stands in for it.\n */\nfunction opensMidTurn(messages: readonly DeepAgentWindowSyncMessage[]): boolean {\n const first = messages[0];\n if (first === undefined) return false;\n if (messages.every((message) => message.role === undefined)) return messages.length < 2;\n if (first.role !== 'user') return true;\n if (messages.every((message) => message.role === 'user')) return false;\n let turns = 0;\n for (let index = 1; index < messages.length; index += 1) {\n if (messages[index - 1]!.role === 'user' && messages[index]!.role !== 'user') turns += 1;\n }\n return turns < 2;\n}\n\nexport function useDeepAgentWindowSync({\n messages,\n pageSize,\n toolOutputs,\n nearTop = false,\n nearTopEpoch = 0,\n isMessageMounted = everyMessageMounted,\n maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,\n maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,\n maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,\n onHistoryLoad,\n onError,\n now = defaultNow,\n}: UseDeepAgentWindowSyncOptions): DeepAgentWindowSyncState {\n const { sendCommand, invoke, connection } = useStatewireExtras();\n const window = useDeepAgentMessageWindow();\n const [loading, setLoading] = useState(false);\n const [requests, setRequests] = useState(0);\n const servedRequestsRef = useRef(0);\n const inflightRef = useRef(false);\n const chainRef = useRef({ epoch: 0, loads: 0, unseen: 0, requested: false });\n // Pages loaded to show the opening turn's prompt; `null` once it shows, the\n // user took over, or a page failed — never re-armed for this mount.\n const openingFillRef = useRef<number | null>(0);\n // The converted thread when the in-flight page was stated; `null` once its\n // settle was judged (or it failed).\n const pageFromRef = useRef<{ count: number; secondId: string | undefined } | null>(null);\n const aliveRef = useRef(true);\n // The interests this mount last stated, re-stated on the next connected\n // session (a hello resets the documents) so a reconnect keeps the loaded history.\n const statedRef = useRef<DeepAgentInterestRequest[] | null>(null);\n const restatedConnectionRef = useRef<unknown>(null);\n // Outputs already asked for and answered by this mount — fetched, refused,\n // or failed — by the `chars` they were asked for. The converter re-runs on\n // every wire change, and the store is a bounded LRU: without this memory a\n // refused read would leave on every pass, and once a transcript holds more\n // cut outputs than the store retains, each fetch would evict a still-loaded\n // output and the next pass would fetch it again, forever. An evicted output\n // renders its cut text (the REST display limit) rather than re-entering the\n // loop; a marker whose `chars` moved (the host replaced the output) is asked\n // for again.\n const answeredRef = useRef(new Map<string, number>());\n // Reads this mount has in flight, and whether a pass left outputs waiting\n // for one to settle (`maxConcurrentToolOutputs`): only then does a settle\n // re-run the pass, so an unbounded mount renders no more than before.\n const toolOutputsInFlightRef = useRef(0);\n const toolOutputsWaitingRef = useRef(false);\n const [toolOutputSettles, setToolOutputSettles] = useState(0);\n const onHistoryLoadRef = useRef(onHistoryLoad);\n onHistoryLoadRef.current = onHistoryLoad;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const nowRef = useRef(now);\n nowRef.current = now;\n useEffect(() => {\n aliveRef.current = true;\n return () => {\n aliveRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (connection.status !== 'connected' || restatedConnectionRef.current === connection) return;\n // Decided once per connected session, on the hello's interest document.\n if (window === null) return;\n restatedConnectionRef.current = connection;\n const stated = statedRef.current;\n if (stated === null || interestsCovered(window.requests, stated)) return;\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [stated]).catch((error: unknown) => {\n onErrorRef.current?.(error, 'athena/interest');\n });\n }, [connection, invoke, window]);\n\n useEffect(() => {\n if (window === null || inflightRef.current) return;\n const from = pageFromRef.current;\n pageFromRef.current = null;\n const lastPageUnseen =\n from !== null && from.count > 0 && !pageShowedEarlierMessage(from, messages);\n const pageRequests = pageInterestRequests(window, pageSize);\n if (pageRequests === null) return;\n const requested = requests !== servedRequestsRef.current;\n const chain = chainRef.current;\n let fillPage = false;\n if (\n openingFillRef.current !== null &&\n (requested || nearTop || (messages.length > 0 && !opensMidTurn(messages)))\n ) {\n openingFillRef.current = null;\n }\n if (requested) {\n // An explicit ask re-arms the chain for this approach.\n servedRequestsRef.current = requests;\n chainRef.current = { epoch: nearTopEpoch, loads: 1, unseen: 0, requested: true };\n } else if (lastPageUnseen && (chain.requested || (nearTop && chain.epoch === nearTopEpoch))) {\n // The page landed inside the first loaded turn: keep going until one shows.\n if (chain.unseen >= maxUnseenLoads) return;\n chain.unseen += 1;\n } else if (openingFillRef.current !== null && opensMidTurn(messages)) {\n if (openingFillRef.current >= maxUnseenLoads) return;\n openingFillRef.current += 1;\n fillPage = true;\n } else {\n if (!nearTop) return;\n if (chain.epoch !== nearTopEpoch) {\n chainRef.current = { epoch: nearTopEpoch, loads: 0, unseen: 0, requested: false };\n }\n if (chainRef.current.loads >= maxChainedLoads) return;\n chainRef.current.loads += 1;\n }\n pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\n onHistoryLoadRef.current?.({\n page,\n request,\n durationMs: Math.max(0, Math.round(nowRef.current() - startedAt)),\n });\n })\n .catch((error: unknown) => {\n pageFromRef.current = null;\n if (fillPage) openingFillRef.current = null;\n onErrorRef.current?.(error, 'athena/interest');\n })\n .finally(() => {\n inflightRef.current = false;\n if (aliveRef.current) setLoading(false);\n });\n // `loading` re-arms the chain once a page settled after its documents moved the window.\n }, [\n nearTop,\n nearTopEpoch,\n requests,\n window,\n loading,\n messages,\n invoke,\n maxChainedLoads,\n maxUnseenLoads,\n pageSize,\n ]);\n\n useEffect(() => {\n if (window === null || toolOutputs === undefined) return;\n toolOutputsWaitingRef.current = false;\n for (const message of messages) {\n if (!isMessageMounted(message.id)) continue;\n for (const part of message.content) {\n if (part.type !== 'tool-call') continue;\n const marker = readTruncatedToolOutputArtifact(part.artifact);\n if (marker === null || toolOutputs.isPending(marker.messageId)) continue;\n // Fetched for this very length already; a different length means the\n // host replaced the output under the same id since.\n if (toolOutputs.entry(marker.messageId)?.chars === marker.chars) continue;\n if (answeredRef.current.get(marker.messageId) === marker.chars) continue;\n if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {\n toolOutputsWaitingRef.current = true;\n return;\n }\n const { messageId, chars } = marker;\n toolOutputsInFlightRef.current += 1;\n toolOutputs.markPending(messageId);\n Promise.resolve(sendCommand(buildToolOutputCommand({ messageId })))\n .then((payload) => {\n answeredRef.current.set(messageId, chars);\n const result = readToolOutputResult(payload);\n if (result === null) {\n toolOutputs.clearPending(messageId);\n return;\n }\n toolOutputs.set(result.messageId, { content: result.content, chars });\n })\n .catch((error: unknown) => {\n answeredRef.current.set(messageId, chars);\n toolOutputs.clearPending(messageId);\n onErrorRef.current?.(error, 'history/tool_output');\n })\n .finally(() => {\n toolOutputsInFlightRef.current -= 1;\n if (toolOutputsWaitingRef.current && aliveRef.current) {\n setToolOutputSettles((count) => count + 1);\n }\n });\n }\n }\n }, [\n messages,\n isMessageMounted,\n window,\n sendCommand,\n toolOutputs,\n maxConcurrentToolOutputs,\n toolOutputSettles,\n ]);\n\n const loadOlder = useCallback(() => {\n setRequests((count) => count + 1);\n }, []);\n\n const canLoadOlder = useMemo(\n () => window !== null && pageInterestRequests(window, pageSize) !== null,\n [window, pageSize]\n );\n\n return { window, hasOlder: window?.hasOlder ?? false, canLoadOlder, loading, loadOlder };\n}\n"],"names":["_a"],"mappings":"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"}
@@ -4,11 +4,129 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const react = require("@assistant-ui/react");
5
5
  const react$1 = require("react");
6
6
  const statewireWindowContext = require("../../provider/statewire-window-context.cjs");
7
+ const threadIdContext = require("../../threads/thread-id-context.cjs");
7
8
  const useDeepAgentWindowSync = require("../../agent-runtime/src/use-deep-agent-window-sync.cjs");
8
9
  const errors = require("../../agent-runtime/src/errors.cjs");
9
10
  const cnfast = require("cnfast");
10
11
  const THREAD_VIEWPORT_ATTR = "data-aui-thread-viewport";
11
12
  const HISTORY_EDGE_NEAR_MARGIN_PX = 1500;
13
+ const HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY = 2;
14
+ const MESSAGE_SELECTOR = "[data-message-id]";
15
+ const NO_MESSAGE_IDS = /* @__PURE__ */ new Set();
16
+ function sameIds({ a, b }) {
17
+ if (a.size !== b.size) return false;
18
+ for (const id of a) if (!b.has(id)) return false;
19
+ return true;
20
+ }
21
+ function useMessagesNearViewport({
22
+ viewport,
23
+ hasViewport,
24
+ messages
25
+ }) {
26
+ const [nearIds, setNearIds] = react$1.useState(NO_MESSAGE_IDS);
27
+ const observable = hasViewport && typeof IntersectionObserver !== "undefined";
28
+ const trackRef = react$1.useRef(null);
29
+ const renderedIdsRef = react$1.useRef(NO_MESSAGE_IDS);
30
+ react$1.useEffect(() => {
31
+ if (!viewport || !observable) return void 0;
32
+ const near = /* @__PURE__ */ new Set();
33
+ const observed = /* @__PURE__ */ new Set();
34
+ const publish = () => {
35
+ const ids = /* @__PURE__ */ new Set();
36
+ for (const element of near) {
37
+ const id = element.getAttribute("data-message-id");
38
+ if (id) ids.add(id);
39
+ }
40
+ setNearIds((previous) => sameIds({ a: previous, b: ids }) ? previous : ids);
41
+ };
42
+ const intersections = new IntersectionObserver(
43
+ (entries) => {
44
+ for (const entry of entries) {
45
+ if (entry.isIntersecting && entry.target.isConnected) near.add(entry.target);
46
+ else near.delete(entry.target);
47
+ }
48
+ publish();
49
+ },
50
+ { root: viewport, rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px` }
51
+ );
52
+ const track = () => {
53
+ let dropped = false;
54
+ for (const element of observed) {
55
+ if (element.isConnected) continue;
56
+ intersections.unobserve(element);
57
+ observed.delete(element);
58
+ dropped = near.delete(element) || dropped;
59
+ }
60
+ const rendered = /* @__PURE__ */ new Set();
61
+ for (const element of viewport.querySelectorAll(MESSAGE_SELECTOR)) {
62
+ const id = element.getAttribute("data-message-id");
63
+ if (id) rendered.add(id);
64
+ if (observed.has(element)) continue;
65
+ observed.add(element);
66
+ intersections.observe(element);
67
+ }
68
+ renderedIdsRef.current = rendered;
69
+ if (dropped) publish();
70
+ };
71
+ trackRef.current = track;
72
+ track();
73
+ return () => {
74
+ trackRef.current = null;
75
+ renderedIdsRef.current = NO_MESSAGE_IDS;
76
+ intersections.disconnect();
77
+ setNearIds(NO_MESSAGE_IDS);
78
+ };
79
+ }, [viewport, observable]);
80
+ react$1.useEffect(() => {
81
+ var _a;
82
+ (_a = trackRef.current) == null ? void 0 : _a.call(trackRef);
83
+ }, [messages]);
84
+ return react$1.useMemo(() => {
85
+ if (!observable) return void 0;
86
+ return (messageId) => {
87
+ if (viewport === null) return false;
88
+ return nearIds.has(messageId) || !renderedIdsRef.current.has(messageId);
89
+ };
90
+ }, [nearIds, observable, viewport]);
91
+ }
92
+ function oldestSourceId(message) {
93
+ const source = react.getExternalStoreMessages(message)[0];
94
+ return typeof (source == null ? void 0 : source.id) === "string" ? source.id : void 0;
95
+ }
96
+ function prependedOlderHistory({
97
+ previous,
98
+ next
99
+ }) {
100
+ const previousFirst = previous[0];
101
+ const nextFirst = next[0];
102
+ if (previousFirst === void 0 || nextFirst === void 0 || previousFirst === nextFirst) return false;
103
+ if (previousFirst.id !== nextFirst.id && next.some((message, index) => index > 0 && message.id === previousFirst.id)) {
104
+ return true;
105
+ }
106
+ const previousOldest = oldestSourceId(previousFirst);
107
+ if (previousOldest === void 0 || previousOldest === oldestSourceId(nextFirst)) return false;
108
+ return react.getExternalStoreMessages(nextFirst).some(
109
+ (source, index) => index > 0 && (source == null ? void 0 : source.id) === previousOldest
110
+ );
111
+ }
112
+ class KeepReadingPlace extends react$1.Component {
113
+ getSnapshotBeforeUpdate(previous) {
114
+ const { viewport, threadId, messages } = this.props;
115
+ if (viewport === null || viewport !== previous.viewport || threadId !== previous.threadId || messages === previous.messages || !prependedOlderHistory({ previous: previous.messages, next: messages })) {
116
+ return null;
117
+ }
118
+ return viewport.scrollHeight - viewport.scrollTop;
119
+ }
120
+ componentDidUpdate(_previous, _state, fromBottom) {
121
+ const { viewport } = this.props;
122
+ if (viewport === null || fromBottom === null) return;
123
+ const top = viewport.scrollHeight - fromBottom;
124
+ if (Math.abs(viewport.scrollTop - top) >= 1) viewport.scrollTo({ top, behavior: "instant" });
125
+ }
126
+ render() {
127
+ return null;
128
+ }
129
+ }
12
130
  const HistoryEdgeRow = ({
13
131
  hasOlder,
14
132
  canLoadOlder = hasOlder,
@@ -45,19 +163,32 @@ const StatewireHistoryEdge = ({
45
163
  className,
46
164
  onHistoryLoad
47
165
  }) => {
48
- var _a;
49
166
  const providerToolOutputs = statewireWindowContext.useStatewireToolOutputs();
50
167
  const toolOutputs = toolOutputsProp ?? providerToolOutputs ?? void 0;
51
168
  const messages = react.useAuiState((s) => s.thread.messages);
169
+ const threadId = react$1.useContext(threadIdContext.AthenaStatewireThreadIdContext);
52
170
  const [nearTop, setNearTop] = react$1.useState(false);
53
171
  const [nearTopEpoch, setNearTopEpoch] = react$1.useState(0);
54
172
  const nearTopRef = react$1.useRef(false);
55
173
  const [rowEl, setRowEl] = react$1.useState(null);
174
+ const hasViewportStore = react.useThreadViewportStore({ optional: true }) !== null;
175
+ const registeredViewport = react.useThreadViewport({
176
+ optional: true,
177
+ selector: (state) => state.element.viewport
178
+ });
179
+ const viewport = registeredViewport ?? (rowEl == null ? void 0 : rowEl.closest(`[${THREAD_VIEWPORT_ATTR}]`)) ?? null;
180
+ const isMessageMounted = useMessagesNearViewport({
181
+ viewport,
182
+ hasViewport: hasViewportStore || viewport !== null,
183
+ messages
184
+ });
56
185
  const { hasOlder, canLoadOlder, loading, loadOlder } = useDeepAgentWindowSync.useDeepAgentWindowSync({
57
186
  messages,
58
187
  toolOutputs,
59
188
  nearTop,
60
189
  nearTopEpoch,
190
+ isMessageMounted,
191
+ maxConcurrentToolOutputs: HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY,
61
192
  onHistoryLoad,
62
193
  onError: (error, command) => {
63
194
  if (error instanceof Error && errors.isStatewireChannelTeardown(error)) return;
@@ -77,7 +208,7 @@ const StatewireHistoryEdge = ({
77
208
  }
78
209
  },
79
210
  {
80
- root: rowEl.closest(`[${THREAD_VIEWPORT_ATTR}]`),
211
+ root: viewport,
81
212
  rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px`
82
213
  }
83
214
  );
@@ -87,32 +218,24 @@ const StatewireHistoryEdge = ({
87
218
  nearTopRef.current = false;
88
219
  setNearTop(false);
89
220
  };
90
- }, [rowEl, hasOlder]);
91
- const shapeRef = react$1.useRef(
92
- null
93
- );
94
- const firstId = (_a = messages[0]) == null ? void 0 : _a.id;
95
- const length = messages.length;
96
- react$1.useLayoutEffect(() => {
97
- const viewport = (rowEl == null ? void 0 : rowEl.closest(`[${THREAD_VIEWPORT_ATTR}]`)) ?? null;
98
- const prev = shapeRef.current;
99
- const prepended = viewport !== null && prev !== null && prev.firstId !== void 0 && firstId !== prev.firstId && length > prev.length;
100
- if (prepended) viewport.scrollTop += viewport.scrollHeight - prev.scrollHeight;
101
- shapeRef.current = loading || prepended || prev === null ? { firstId, length, scrollHeight: (viewport == null ? void 0 : viewport.scrollHeight) ?? 0 } : { firstId, length, scrollHeight: prev.scrollHeight };
102
- });
103
- return /* @__PURE__ */ jsxRuntime.jsx(
104
- HistoryEdgeRow,
105
- {
106
- rowRef: setRowEl,
107
- hasOlder,
108
- canLoadOlder,
109
- loading,
110
- onLoadOlder: loadOlder,
111
- className
112
- }
113
- );
221
+ }, [rowEl, hasOlder, viewport]);
222
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
223
+ /* @__PURE__ */ jsxRuntime.jsx(KeepReadingPlace, { viewport, threadId, messages }),
224
+ /* @__PURE__ */ jsxRuntime.jsx(
225
+ HistoryEdgeRow,
226
+ {
227
+ rowRef: setRowEl,
228
+ hasOlder,
229
+ canLoadOlder,
230
+ loading,
231
+ onLoadOlder: loadOlder,
232
+ className
233
+ }
234
+ )
235
+ ] });
114
236
  };
115
237
  exports.HISTORY_EDGE_NEAR_MARGIN_PX = HISTORY_EDGE_NEAR_MARGIN_PX;
238
+ exports.HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY = HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY;
116
239
  exports.HistoryEdgeRow = HistoryEdgeRow;
117
240
  exports.StatewireHistoryEdge = StatewireHistoryEdge;
118
241
  exports.THREAD_VIEWPORT_ATTR = THREAD_VIEWPORT_ATTR;
@@ -1 +1 @@
1
- {"version":3,"file":"statewire-history-edge.cjs","sources":["../../../src/chat-ui/thread/statewire-history-edge.tsx"],"sourcesContent":["import { useAuiState } 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 { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { useStatewireToolOutputs } from \"../../provider/statewire-window-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.\n */\nexport const HISTORY_EDGE_NEAR_MARGIN_PX = 1500;\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 \"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 rendered cards, so they re-render\n * 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 [nearTop, setNearTop] = useState(false);\n const [nearTopEpoch, setNearTopEpoch] = useState(0);\n const nearTopRef = useRef(false);\n const [rowEl, setRowEl] = useState<HTMLElement | null>(null);\n const { hasOlder, canLoadOlder, loading, loadOlder } = useDeepAgentWindowSync({\n messages,\n toolOutputs,\n nearTop,\n nearTopEpoch,\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 // 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: rowEl.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`),\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]);\n\n // A prepend (older history landing) keeps the reading position: the\n // viewport scrolls by the height the page added above the view, so the\n // message the user was on stays put. The pre-prepend height is measured\n // only while a page is in flight (a layout read per render would force a\n // reflow on every streamed token otherwise).\n const shapeRef = useRef<{ firstId: string | undefined; length: number; scrollHeight: number } | null>(\n null,\n );\n const firstId = messages[0]?.id;\n const length = messages.length;\n useLayoutEffect(() => {\n const viewport = rowEl?.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ?? null;\n const prev = shapeRef.current;\n const prepended =\n viewport !== null &&\n prev !== null &&\n prev.firstId !== undefined &&\n firstId !== prev.firstId &&\n length > prev.length;\n if (prepended) viewport.scrollTop += viewport.scrollHeight - prev.scrollHeight;\n shapeRef.current =\n loading || prepended || prev === null\n ? { firstId, length, scrollHeight: viewport?.scrollHeight ?? 0 }\n : { firstId, length, scrollHeight: prev.scrollHeight };\n });\n\n return (\n <HistoryEdgeRow\n rowRef={setRowEl}\n hasOlder={hasOlder}\n canLoadOlder={canLoadOlder}\n loading={loading}\n onLoadOlder={loadOlder}\n className={className}\n />\n );\n};\n"],"names":["jsx","cn","useStatewireToolOutputs","useAuiState","useState","useRef","useDeepAgentWindowSync","isStatewireChannelTeardown","useEffect","useLayoutEffect"],"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 { 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 // Messages remount when a page folds into them (a new id), so what is\n // observed follows the rendered elements, not the message list.\n const track = () => {\n let dropped = false;\n for (const element of observed) {\n if (element.isConnected) continue;\n intersections.unobserve(element);\n observed.delete(element);\n dropped = near.delete(element) || dropped;\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 if (dropped) 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 (that\n * message remounts under a new id, taking the browser's anchor node with it).\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 \"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 { 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 // 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","useDeepAgentWindowSync","isStatewireChannelTeardown","jsxs","Fragment"],"mappings":"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"}
@@ -5,9 +5,16 @@ export declare const THREAD_VIEWPORT_ATTR = "data-aui-thread-viewport";
5
5
  /**
6
6
  * How far above the viewport the edge row counts as near: the row sits above
7
7
  * the first loaded message, so reaching it means the user has scrolled past
8
- * everything loaded and the next page should already be on its way.
8
+ * everything loaded and the next page should already be on its way. Messages
9
+ * this close to the viewport also count as on screen for cut tool outputs.
9
10
  */
10
11
  export declare const HISTORY_EDGE_NEAR_MARGIN_PX = 1500;
12
+ /**
13
+ * Cut tool outputs a transcript reads at once. The reads share the session's
14
+ * command lane with everything the person sends, so a long transcript must
15
+ * not queue dozens of them ahead of a message or a Stop.
16
+ */
17
+ export declare const HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY = 2;
11
18
  export interface HistoryEdgeRowProps {
12
19
  /** Whether the server holds messages older than the loaded window. */
13
20
  hasOlder: boolean;
@@ -45,8 +52,8 @@ export interface StatewireHistoryEdgeProps {
45
52
  * - renders the "Load earlier messages" edge row and loads the next page when
46
53
  * the row comes near the viewport (scrolling up) or on its button, keeping
47
54
  * the reading position when the page lands above the view;
48
- * - fetches cut tool outputs whole for the rendered cards, so they re-render
49
- * complete through the converter (`toolOutputs`).
55
+ * - fetches cut tool outputs whole for the cards near the viewport, a few at
56
+ * a time, so they re-render complete through the converter (`toolOutputs`).
50
57
  *
51
58
  * Inert until the head interest resolves: no row, no reads.
52
59
  */
@@ -1,12 +1,130 @@
1
- import { jsx } from "react/jsx-runtime";
2
- import { useAuiState } from "@assistant-ui/react";
3
- import { useState, useRef, useEffect, useLayoutEffect } from "react";
1
+ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
+ import { useAuiState, useThreadViewportStore, useThreadViewport, getExternalStoreMessages } from "@assistant-ui/react";
3
+ import { useContext, useState, useRef, useEffect, useMemo, Component } from "react";
4
4
  import { useStatewireToolOutputs } from "../../provider/statewire-window-context.js";
5
+ import { AthenaStatewireThreadIdContext } from "../../threads/thread-id-context.js";
5
6
  import { useDeepAgentWindowSync } from "../../agent-runtime/src/use-deep-agent-window-sync.js";
6
7
  import { isStatewireChannelTeardown } from "../../agent-runtime/src/errors.js";
7
8
  import { cn } from "cnfast";
8
9
  const THREAD_VIEWPORT_ATTR = "data-aui-thread-viewport";
9
10
  const HISTORY_EDGE_NEAR_MARGIN_PX = 1500;
11
+ const HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY = 2;
12
+ const MESSAGE_SELECTOR = "[data-message-id]";
13
+ const NO_MESSAGE_IDS = /* @__PURE__ */ new Set();
14
+ function sameIds({ a, b }) {
15
+ if (a.size !== b.size) return false;
16
+ for (const id of a) if (!b.has(id)) return false;
17
+ return true;
18
+ }
19
+ function useMessagesNearViewport({
20
+ viewport,
21
+ hasViewport,
22
+ messages
23
+ }) {
24
+ const [nearIds, setNearIds] = useState(NO_MESSAGE_IDS);
25
+ const observable = hasViewport && typeof IntersectionObserver !== "undefined";
26
+ const trackRef = useRef(null);
27
+ const renderedIdsRef = useRef(NO_MESSAGE_IDS);
28
+ useEffect(() => {
29
+ if (!viewport || !observable) return void 0;
30
+ const near = /* @__PURE__ */ new Set();
31
+ const observed = /* @__PURE__ */ new Set();
32
+ const publish = () => {
33
+ const ids = /* @__PURE__ */ new Set();
34
+ for (const element of near) {
35
+ const id = element.getAttribute("data-message-id");
36
+ if (id) ids.add(id);
37
+ }
38
+ setNearIds((previous) => sameIds({ a: previous, b: ids }) ? previous : ids);
39
+ };
40
+ const intersections = new IntersectionObserver(
41
+ (entries) => {
42
+ for (const entry of entries) {
43
+ if (entry.isIntersecting && entry.target.isConnected) near.add(entry.target);
44
+ else near.delete(entry.target);
45
+ }
46
+ publish();
47
+ },
48
+ { root: viewport, rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px` }
49
+ );
50
+ const track = () => {
51
+ let dropped = false;
52
+ for (const element of observed) {
53
+ if (element.isConnected) continue;
54
+ intersections.unobserve(element);
55
+ observed.delete(element);
56
+ dropped = near.delete(element) || dropped;
57
+ }
58
+ const rendered = /* @__PURE__ */ new Set();
59
+ for (const element of viewport.querySelectorAll(MESSAGE_SELECTOR)) {
60
+ const id = element.getAttribute("data-message-id");
61
+ if (id) rendered.add(id);
62
+ if (observed.has(element)) continue;
63
+ observed.add(element);
64
+ intersections.observe(element);
65
+ }
66
+ renderedIdsRef.current = rendered;
67
+ if (dropped) publish();
68
+ };
69
+ trackRef.current = track;
70
+ track();
71
+ return () => {
72
+ trackRef.current = null;
73
+ renderedIdsRef.current = NO_MESSAGE_IDS;
74
+ intersections.disconnect();
75
+ setNearIds(NO_MESSAGE_IDS);
76
+ };
77
+ }, [viewport, observable]);
78
+ useEffect(() => {
79
+ var _a;
80
+ (_a = trackRef.current) == null ? void 0 : _a.call(trackRef);
81
+ }, [messages]);
82
+ return useMemo(() => {
83
+ if (!observable) return void 0;
84
+ return (messageId) => {
85
+ if (viewport === null) return false;
86
+ return nearIds.has(messageId) || !renderedIdsRef.current.has(messageId);
87
+ };
88
+ }, [nearIds, observable, viewport]);
89
+ }
90
+ function oldestSourceId(message) {
91
+ const source = getExternalStoreMessages(message)[0];
92
+ return typeof (source == null ? void 0 : source.id) === "string" ? source.id : void 0;
93
+ }
94
+ function prependedOlderHistory({
95
+ previous,
96
+ next
97
+ }) {
98
+ const previousFirst = previous[0];
99
+ const nextFirst = next[0];
100
+ if (previousFirst === void 0 || nextFirst === void 0 || previousFirst === nextFirst) return false;
101
+ if (previousFirst.id !== nextFirst.id && next.some((message, index) => index > 0 && message.id === previousFirst.id)) {
102
+ return true;
103
+ }
104
+ const previousOldest = oldestSourceId(previousFirst);
105
+ if (previousOldest === void 0 || previousOldest === oldestSourceId(nextFirst)) return false;
106
+ return getExternalStoreMessages(nextFirst).some(
107
+ (source, index) => index > 0 && (source == null ? void 0 : source.id) === previousOldest
108
+ );
109
+ }
110
+ class KeepReadingPlace extends Component {
111
+ getSnapshotBeforeUpdate(previous) {
112
+ const { viewport, threadId, messages } = this.props;
113
+ if (viewport === null || viewport !== previous.viewport || threadId !== previous.threadId || messages === previous.messages || !prependedOlderHistory({ previous: previous.messages, next: messages })) {
114
+ return null;
115
+ }
116
+ return viewport.scrollHeight - viewport.scrollTop;
117
+ }
118
+ componentDidUpdate(_previous, _state, fromBottom) {
119
+ const { viewport } = this.props;
120
+ if (viewport === null || fromBottom === null) return;
121
+ const top = viewport.scrollHeight - fromBottom;
122
+ if (Math.abs(viewport.scrollTop - top) >= 1) viewport.scrollTo({ top, behavior: "instant" });
123
+ }
124
+ render() {
125
+ return null;
126
+ }
127
+ }
10
128
  const HistoryEdgeRow = ({
11
129
  hasOlder,
12
130
  canLoadOlder = hasOlder,
@@ -43,19 +161,32 @@ const StatewireHistoryEdge = ({
43
161
  className,
44
162
  onHistoryLoad
45
163
  }) => {
46
- var _a;
47
164
  const providerToolOutputs = useStatewireToolOutputs();
48
165
  const toolOutputs = toolOutputsProp ?? providerToolOutputs ?? void 0;
49
166
  const messages = useAuiState((s) => s.thread.messages);
167
+ const threadId = useContext(AthenaStatewireThreadIdContext);
50
168
  const [nearTop, setNearTop] = useState(false);
51
169
  const [nearTopEpoch, setNearTopEpoch] = useState(0);
52
170
  const nearTopRef = useRef(false);
53
171
  const [rowEl, setRowEl] = useState(null);
172
+ const hasViewportStore = useThreadViewportStore({ optional: true }) !== null;
173
+ const registeredViewport = useThreadViewport({
174
+ optional: true,
175
+ selector: (state) => state.element.viewport
176
+ });
177
+ const viewport = registeredViewport ?? (rowEl == null ? void 0 : rowEl.closest(`[${THREAD_VIEWPORT_ATTR}]`)) ?? null;
178
+ const isMessageMounted = useMessagesNearViewport({
179
+ viewport,
180
+ hasViewport: hasViewportStore || viewport !== null,
181
+ messages
182
+ });
54
183
  const { hasOlder, canLoadOlder, loading, loadOlder } = useDeepAgentWindowSync({
55
184
  messages,
56
185
  toolOutputs,
57
186
  nearTop,
58
187
  nearTopEpoch,
188
+ isMessageMounted,
189
+ maxConcurrentToolOutputs: HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY,
59
190
  onHistoryLoad,
60
191
  onError: (error, command) => {
61
192
  if (error instanceof Error && isStatewireChannelTeardown(error)) return;
@@ -75,7 +206,7 @@ const StatewireHistoryEdge = ({
75
206
  }
76
207
  },
77
208
  {
78
- root: rowEl.closest(`[${THREAD_VIEWPORT_ATTR}]`),
209
+ root: viewport,
79
210
  rootMargin: `${HISTORY_EDGE_NEAR_MARGIN_PX}px 0px`
80
211
  }
81
212
  );
@@ -85,33 +216,25 @@ const StatewireHistoryEdge = ({
85
216
  nearTopRef.current = false;
86
217
  setNearTop(false);
87
218
  };
88
- }, [rowEl, hasOlder]);
89
- const shapeRef = useRef(
90
- null
91
- );
92
- const firstId = (_a = messages[0]) == null ? void 0 : _a.id;
93
- const length = messages.length;
94
- useLayoutEffect(() => {
95
- const viewport = (rowEl == null ? void 0 : rowEl.closest(`[${THREAD_VIEWPORT_ATTR}]`)) ?? null;
96
- const prev = shapeRef.current;
97
- const prepended = viewport !== null && prev !== null && prev.firstId !== void 0 && firstId !== prev.firstId && length > prev.length;
98
- if (prepended) viewport.scrollTop += viewport.scrollHeight - prev.scrollHeight;
99
- shapeRef.current = loading || prepended || prev === null ? { firstId, length, scrollHeight: (viewport == null ? void 0 : viewport.scrollHeight) ?? 0 } : { firstId, length, scrollHeight: prev.scrollHeight };
100
- });
101
- return /* @__PURE__ */ jsx(
102
- HistoryEdgeRow,
103
- {
104
- rowRef: setRowEl,
105
- hasOlder,
106
- canLoadOlder,
107
- loading,
108
- onLoadOlder: loadOlder,
109
- className
110
- }
111
- );
219
+ }, [rowEl, hasOlder, viewport]);
220
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
221
+ /* @__PURE__ */ jsx(KeepReadingPlace, { viewport, threadId, messages }),
222
+ /* @__PURE__ */ jsx(
223
+ HistoryEdgeRow,
224
+ {
225
+ rowRef: setRowEl,
226
+ hasOlder,
227
+ canLoadOlder,
228
+ loading,
229
+ onLoadOlder: loadOlder,
230
+ className
231
+ }
232
+ )
233
+ ] });
112
234
  };
113
235
  export {
114
236
  HISTORY_EDGE_NEAR_MARGIN_PX,
237
+ HISTORY_EDGE_TOOL_OUTPUT_CONCURRENCY,
115
238
  HistoryEdgeRow,
116
239
  StatewireHistoryEdge,
117
240
  THREAD_VIEWPORT_ATTR
@@ -1 +1 @@
1
- {"version":3,"file":"statewire-history-edge.js","sources":["../../../src/chat-ui/thread/statewire-history-edge.tsx"],"sourcesContent":["import { useAuiState } 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 { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { useStatewireToolOutputs } from \"../../provider/statewire-window-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.\n */\nexport const HISTORY_EDGE_NEAR_MARGIN_PX = 1500;\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 \"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 rendered cards, so they re-render\n * 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 [nearTop, setNearTop] = useState(false);\n const [nearTopEpoch, setNearTopEpoch] = useState(0);\n const nearTopRef = useRef(false);\n const [rowEl, setRowEl] = useState<HTMLElement | null>(null);\n const { hasOlder, canLoadOlder, loading, loadOlder } = useDeepAgentWindowSync({\n messages,\n toolOutputs,\n nearTop,\n nearTopEpoch,\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 // 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: rowEl.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`),\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]);\n\n // A prepend (older history landing) keeps the reading position: the\n // viewport scrolls by the height the page added above the view, so the\n // message the user was on stays put. The pre-prepend height is measured\n // only while a page is in flight (a layout read per render would force a\n // reflow on every streamed token otherwise).\n const shapeRef = useRef<{ firstId: string | undefined; length: number; scrollHeight: number } | null>(\n null,\n );\n const firstId = messages[0]?.id;\n const length = messages.length;\n useLayoutEffect(() => {\n const viewport = rowEl?.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ?? null;\n const prev = shapeRef.current;\n const prepended =\n viewport !== null &&\n prev !== null &&\n prev.firstId !== undefined &&\n firstId !== prev.firstId &&\n length > prev.length;\n if (prepended) viewport.scrollTop += viewport.scrollHeight - prev.scrollHeight;\n shapeRef.current =\n loading || prepended || prev === null\n ? { firstId, length, scrollHeight: viewport?.scrollHeight ?? 0 }\n : { firstId, length, scrollHeight: prev.scrollHeight };\n });\n\n return (\n <HistoryEdgeRow\n rowRef={setRowEl}\n hasOlder={hasOlder}\n canLoadOlder={canLoadOlder}\n loading={loading}\n onLoadOlder={loadOlder}\n className={className}\n />\n );\n};\n"],"names":[],"mappings":"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"}
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 { 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 // Messages remount when a page folds into them (a new id), so what is\n // observed follows the rendered elements, not the message list.\n const track = () => {\n let dropped = false;\n for (const element of observed) {\n if (element.isConnected) continue;\n intersections.unobserve(element);\n observed.delete(element);\n dropped = near.delete(element) || dropped;\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 if (dropped) 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 (that\n * message remounts under a new id, taking the browser's anchor node with it).\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 \"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 { 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 // 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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"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const version = "0.36.0";
3
+ const version = "0.36.1";
4
4
  const packageJson = {
5
5
  version
6
6
  };
@@ -1,4 +1,4 @@
1
- const version = "0.36.0";
1
+ const version = "0.36.1";
2
2
  const packageJson = {
3
3
  version
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@athenaintel/react",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
4
4
  "description": "Athena React SDK — Build AI-powered chat applications with the Athena platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",