@athenaintel/react 0.53.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +24 -1
  2. package/dist/agent-runtime/src/call-recap.cjs +2 -3
  3. package/dist/agent-runtime/src/call-recap.cjs.map +1 -1
  4. package/dist/agent-runtime/src/call-recap.d.ts +1 -2
  5. package/dist/agent-runtime/src/call-recap.js +1 -2
  6. package/dist/agent-runtime/src/call-recap.js.map +1 -1
  7. package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs +7 -3
  8. package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs.map +1 -1
  9. package/dist/agent-runtime/src/deep-agent-transport-recipe.d.ts +10 -2
  10. package/dist/agent-runtime/src/deep-agent-transport-recipe.js +8 -4
  11. package/dist/agent-runtime/src/deep-agent-transport-recipe.js.map +1 -1
  12. package/dist/agent-runtime/src/desktop-handoff.d.ts +7 -0
  13. package/dist/agent-runtime/src/dictation-session.d.ts +158 -0
  14. package/dist/agent-runtime/src/edit-command.cjs +6 -0
  15. package/dist/agent-runtime/src/edit-command.cjs.map +1 -1
  16. package/dist/agent-runtime/src/edit-command.d.ts +8 -0
  17. package/dist/agent-runtime/src/edit-command.js +6 -0
  18. package/dist/agent-runtime/src/edit-command.js.map +1 -1
  19. package/dist/agent-runtime/src/generated/runconfig-contract.cjs +6 -0
  20. package/dist/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
  21. package/dist/agent-runtime/src/generated/runconfig-contract.d.ts +13 -0
  22. package/dist/agent-runtime/src/generated/runconfig-contract.js +6 -0
  23. package/dist/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
  24. package/dist/agent-runtime/src/index.d.ts +6 -6
  25. package/dist/agent-runtime/src/message-window.cjs +111 -7
  26. package/dist/agent-runtime/src/message-window.cjs.map +1 -1
  27. package/dist/agent-runtime/src/message-window.d.ts +66 -6
  28. package/dist/agent-runtime/src/message-window.js +112 -8
  29. package/dist/agent-runtime/src/message-window.js.map +1 -1
  30. package/dist/agent-runtime/src/offline-status.cjs.map +1 -1
  31. package/dist/agent-runtime/src/offline-status.d.ts +33 -0
  32. package/dist/agent-runtime/src/offline-status.js.map +1 -1
  33. package/dist/agent-runtime/src/runtime.d.ts +4 -3
  34. package/dist/agent-runtime/src/subagent-transcripts.cjs +23 -1
  35. package/dist/agent-runtime/src/subagent-transcripts.cjs.map +1 -1
  36. package/dist/agent-runtime/src/subagent-transcripts.js +25 -3
  37. package/dist/agent-runtime/src/subagent-transcripts.js.map +1 -1
  38. package/dist/agent-runtime/src/subagents.cjs +112 -0
  39. package/dist/agent-runtime/src/subagents.cjs.map +1 -1
  40. package/dist/agent-runtime/src/subagents.d.ts +103 -0
  41. package/dist/agent-runtime/src/subagents.js +113 -1
  42. package/dist/agent-runtime/src/subagents.js.map +1 -1
  43. package/dist/agent-runtime/src/thread-handoff-protocol.d.ts +37 -9
  44. package/dist/agent-runtime/src/thread-handoff.d.ts +1 -1
  45. package/dist/agent-runtime/src/tool-metadata.cjs +8 -0
  46. package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
  47. package/dist/agent-runtime/src/tool-metadata.js +8 -0
  48. package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
  49. package/dist/agent-runtime/src/use-deep-agent-thread.cjs +6 -0
  50. package/dist/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
  51. package/dist/agent-runtime/src/use-deep-agent-thread.js +7 -1
  52. package/dist/agent-runtime/src/use-deep-agent-thread.js.map +1 -1
  53. package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +39 -10
  54. package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
  55. package/dist/agent-runtime/src/use-deep-agent-window-sync.d.ts +14 -4
  56. package/dist/agent-runtime/src/use-deep-agent-window-sync.js +41 -12
  57. package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
  58. package/dist/agent-runtime/src/use-thread-handoff-flow.d.ts +5 -9
  59. package/dist/agent-runtime/src/voice-presentation.cjs +34 -0
  60. package/dist/agent-runtime/src/voice-presentation.cjs.map +1 -1
  61. package/dist/agent-runtime/src/voice-presentation.d.ts +35 -0
  62. package/dist/agent-runtime/src/voice-presentation.js +34 -0
  63. package/dist/agent-runtime/src/voice-presentation.js.map +1 -1
  64. package/dist/assets/AthenaAssetEmbed.cjs +4 -2
  65. package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
  66. package/dist/assets/AthenaAssetEmbed.d.ts +17 -8
  67. package/dist/assets/AthenaAssetEmbed.js +4 -2
  68. package/dist/assets/AthenaAssetEmbed.js.map +1 -1
  69. package/dist/assets/use-asset-embed.cjs.map +1 -1
  70. package/dist/assets/use-asset-embed.d.ts +7 -5
  71. package/dist/assets/use-asset-embed.js.map +1 -1
  72. package/dist/auth/account-identity.cjs +69 -0
  73. package/dist/auth/account-identity.cjs.map +1 -0
  74. package/dist/auth/account-identity.d.ts +57 -0
  75. package/dist/auth/account-identity.js +69 -0
  76. package/dist/auth/account-identity.js.map +1 -0
  77. package/dist/chat/AthenaChat.cjs +1 -1
  78. package/dist/chat/AthenaChat.cjs.map +1 -1
  79. package/dist/chat/AthenaChat.js +1 -1
  80. package/dist/chat/AthenaChat.js.map +1 -1
  81. package/dist/chat/super-grouping/toolkit-map.cjs +1 -0
  82. package/dist/chat/super-grouping/toolkit-map.cjs.map +1 -1
  83. package/dist/chat/super-grouping/toolkit-map.js +1 -0
  84. package/dist/chat/super-grouping/toolkit-map.js.map +1 -1
  85. package/dist/chat/use-statewire-approval-request.cjs +5 -5
  86. package/dist/chat/use-statewire-approval-request.cjs.map +1 -1
  87. package/dist/chat/use-statewire-approval-request.d.ts +7 -0
  88. package/dist/chat/use-statewire-approval-request.js +5 -5
  89. package/dist/chat/use-statewire-approval-request.js.map +1 -1
  90. package/dist/chat-ui/mentions/use-account-workspace-id.cjs +18 -58
  91. package/dist/chat-ui/mentions/use-account-workspace-id.cjs.map +1 -1
  92. package/dist/chat-ui/mentions/use-account-workspace-id.js +18 -58
  93. package/dist/chat-ui/mentions/use-account-workspace-id.js.map +1 -1
  94. package/dist/chat-ui/messages/voice-delegation-card.cjs +93 -25
  95. package/dist/chat-ui/messages/voice-delegation-card.cjs.map +1 -1
  96. package/dist/chat-ui/messages/voice-delegation-card.d.ts +16 -7
  97. package/dist/chat-ui/messages/voice-delegation-card.js +95 -27
  98. package/dist/chat-ui/messages/voice-delegation-card.js.map +1 -1
  99. package/dist/chat-ui/thread/handoff-cursor.cjs +30 -5
  100. package/dist/chat-ui/thread/handoff-cursor.cjs.map +1 -1
  101. package/dist/chat-ui/thread/handoff-cursor.d.ts +13 -5
  102. package/dist/chat-ui/thread/handoff-cursor.js +32 -7
  103. package/dist/chat-ui/thread/handoff-cursor.js.map +1 -1
  104. package/dist/collab/client.cjs +94 -6
  105. package/dist/collab/client.cjs.map +1 -1
  106. package/dist/collab/client.d.ts +27 -0
  107. package/dist/collab/client.js +94 -6
  108. package/dist/collab/client.js.map +1 -1
  109. package/dist/collab/index.d.ts +1 -1
  110. package/dist/keryx-http/src/initial-sync.cjs +121 -0
  111. package/dist/keryx-http/src/initial-sync.cjs.map +1 -0
  112. package/dist/keryx-http/src/initial-sync.d.ts +77 -0
  113. package/dist/keryx-http/src/initial-sync.js +104 -0
  114. package/dist/keryx-http/src/initial-sync.js.map +1 -0
  115. package/dist/keryx-http/src/unconfirmed-update-attributions.cjs +8 -0
  116. package/dist/keryx-http/src/unconfirmed-update-attributions.cjs.map +1 -1
  117. package/dist/keryx-http/src/unconfirmed-update-attributions.d.ts +6 -0
  118. package/dist/keryx-http/src/unconfirmed-update-attributions.js +8 -0
  119. package/dist/keryx-http/src/unconfirmed-update-attributions.js.map +1 -1
  120. package/dist/react/package.json.cjs +1 -1
  121. package/dist/react/package.json.js +1 -1
  122. package/dist/styles.css +1 -1
  123. package/dist/threads/api.cjs +1 -2
  124. package/dist/threads/api.cjs.map +1 -1
  125. package/dist/threads/api.d.ts +3 -2
  126. package/dist/threads/api.js +1 -2
  127. package/dist/threads/api.js.map +1 -1
  128. package/dist/threads/statewire-thread-list.cjs +5 -7
  129. package/dist/threads/statewire-thread-list.cjs.map +1 -1
  130. package/dist/threads/statewire-thread-list.js +7 -9
  131. package/dist/threads/statewire-thread-list.js.map +1 -1
  132. package/dist/tools/subagent-task-tool-ui.cjs +15 -5
  133. package/dist/tools/subagent-task-tool-ui.cjs.map +1 -1
  134. package/dist/tools/subagent-task-tool-ui.d.ts +5 -4
  135. package/dist/tools/subagent-task-tool-ui.js +16 -6
  136. package/dist/tools/subagent-task-tool-ui.js.map +1 -1
  137. package/package.json +2 -2
@@ -36,20 +36,28 @@ interface ThreadSnapshot {
36
36
  * Opens the transcript at `cursor`: looks for the message every
37
37
  * {@link HANDOFF_CURSOR_POLL_MS}, pages older history in while it is further
38
38
  * back, scrolls to it, holds it in view while the transcript settles (until
39
- * the person scrolls), and highlights it. Calls `onSettled` once it revealed
40
- * the message or gave up (the bottom). Returns its cancel.
39
+ * the person scrolls), and highlights it. `claim` takes the pickup right
40
+ * before the scroll (`false`: another transcript showing the conversation took
41
+ * it, and this one stays put). Calls `onSettled` once it gave up (the bottom)
42
+ * or its hold ended. Returns its cancel, which leaves a revealed message's
43
+ * hold and highlight to finish on their own unless `endHold` (a newer pickup
44
+ * took over).
41
45
  */
42
- export declare function startHandoffCursorReveal({ cursor, readThread, readViewport, onSettled, now, }: {
46
+ export declare function startHandoffCursorReveal({ cursor, readThread, readViewport, claim, onSettled, now, }: {
43
47
  cursor: string;
44
48
  readThread: () => ThreadSnapshot;
45
49
  readViewport: () => HTMLElement | null;
50
+ claim?: () => boolean;
46
51
  onSettled: () => void;
47
52
  now?: () => number;
48
- }): () => void;
53
+ }): (options?: {
54
+ endHold?: boolean;
55
+ }) => void;
49
56
  /**
50
57
  * Opens `threadId`'s transcript at the message the source was reading, once,
51
58
  * when this surface picked the conversation up with a cursor
52
- * (`recordHandoffArrival({ cursor })`). Renders nothing.
59
+ * (`recordHandoffArrival({ cursor })`). When two transcripts show the
60
+ * conversation, the first to find the message owns the pickup. Renders nothing.
53
61
  */
54
62
  export declare const HandoffCursorReveal: FC<{
55
63
  threadId: string | null | undefined;
@@ -1,9 +1,9 @@
1
1
  import { useAui, useThreadViewport, useThreadViewportStore } from "@assistant-ui/react";
2
2
  import { useRef, useEffect, useCallback } from "react";
3
3
  import { THREAD_VIEWPORT_ATTR, THREAD_VIEWPORT_FOOTER_ATTR } from "./statewire-history-edge.js";
4
- import { useHandoffArrivalCursor, settleHandoffArrivalCursor } from "../../agent-runtime/src/handoff-arrival.js";
4
+ import { useHandoffArrivalCursor, readHandoffArrival, settleHandoffArrivalCursor } from "../../agent-runtime/src/handoff-arrival.js";
5
5
  import { readingCursorOf, handoffCursorMessageId, MESSAGE_ROW_ID_ATTRIBUTE, handoffCursorRevealStep, HANDOFF_CURSOR_HIGHLIGHT_MS, HANDOFF_CURSOR_SETTLE_MS } from "../../agent-runtime/src/handoff-cursor.js";
6
- import { deepAgentHistoryHasOlder, loadOlderDeepAgentHistoryPage, readDeepAgentStatewireExtras } from "../../agent-runtime/src/edit-command.js";
6
+ import { deepAgentHistoryHasOlder, loadOlderDeepAgentHistoryPage, isHistoryPageLostToReconnect, readDeepAgentStatewireExtras } from "../../agent-runtime/src/edit-command.js";
7
7
  const THREAD_ROOT_SELECTOR = "[data-aui-thread-root]";
8
8
  const HANDOFF_CURSOR_REVEALED_ATTR = "data-handoff-cursor-revealed";
9
9
  const HANDOFF_CURSOR_POLL_MS = 150;
@@ -84,6 +84,7 @@ function startHandoffCursorReveal({
84
84
  cursor,
85
85
  readThread,
86
86
  readViewport,
87
+ claim = () => true,
87
88
  onSettled,
88
89
  now = () => Date.now()
89
90
  }) {
@@ -114,8 +115,8 @@ function startHandoffCursorReveal({
114
115
  }
115
116
  pagesLoaded += 1;
116
117
  waitingSince = now();
117
- }).catch(() => {
118
- pagesLoaded = Number.POSITIVE_INFINITY;
118
+ }).catch((error) => {
119
+ if (!isHistoryPageLostToReconnect(error)) pagesLoaded = Number.POSITIVE_INFINITY;
119
120
  }).finally(() => {
120
121
  paging = false;
121
122
  pageLandsAt = now() + 2 * HANDOFF_CURSOR_POLL_MS;
@@ -148,6 +149,11 @@ function startHandoffCursorReveal({
148
149
  switch (step) {
149
150
  case "reveal": {
150
151
  if (!viewport || !element) return;
152
+ if (!claim()) {
153
+ settled = true;
154
+ clearInterval(timer);
155
+ return;
156
+ }
151
157
  scrollMessageIntoView({ viewport, element });
152
158
  unhighlight = highlightMessage(element);
153
159
  revealed = { element, viewport, at: now() };
@@ -173,8 +179,10 @@ function startHandoffCursorReveal({
173
179
  };
174
180
  const timer = setInterval(tick, HANDOFF_CURSOR_POLL_MS);
175
181
  tick();
176
- return () => {
182
+ return ({ endHold = false } = {}) => {
183
+ if (endHold) unhighlight == null ? void 0 : unhighlight();
177
184
  if (settled) return;
185
+ if (revealed && !endHold) return;
178
186
  settled = true;
179
187
  clearInterval(timer);
180
188
  detach == null ? void 0 : detach();
@@ -190,17 +198,34 @@ const HandoffCursorReveal = ({ threadId }) => {
190
198
  });
191
199
  const viewportRef = useRef(viewport);
192
200
  viewportRef.current = viewport;
201
+ const latestRef = useRef(null);
193
202
  useEffect(() => {
203
+ var _a;
194
204
  if (!threadId || !cursor) return void 0;
195
- return startHandoffCursorReveal({
205
+ (_a = latestRef.current) == null ? void 0 : _a.call(latestRef, { endHold: true });
206
+ const cancel = startHandoffCursorReveal({
196
207
  cursor,
197
208
  readThread: () => {
198
209
  const state = aui.thread().getState();
199
210
  return { messages: state.messages, isLoading: state.isLoading, extras: state.extras };
200
211
  },
201
212
  readViewport: () => viewportRef.current ?? null,
202
- onSettled: () => settleHandoffArrivalCursor(threadId)
213
+ // Settled before the scroll, so a second transcript's reveal stops at its claim.
214
+ claim: () => {
215
+ var _a2;
216
+ if (((_a2 = readHandoffArrival(threadId)) == null ? void 0 : _a2.cursor) !== cursor) return false;
217
+ settleHandoffArrivalCursor(threadId);
218
+ return true;
219
+ },
220
+ // Only this pickup's: a revealed message's hold ends after its claim
221
+ // settled the cursor, when a later handover may have recorded another.
222
+ onSettled: () => {
223
+ var _a2;
224
+ if (((_a2 = readHandoffArrival(threadId)) == null ? void 0 : _a2.cursor) === cursor) settleHandoffArrivalCursor(threadId);
225
+ }
203
226
  });
227
+ latestRef.current = cancel;
228
+ return cancel;
204
229
  }, [aui, cursor, threadId]);
205
230
  return null;
206
231
  };
@@ -1 +1 @@
1
- {"version":3,"file":"handoff-cursor.js","sources":["../../../src/chat-ui/thread/handoff-cursor.tsx"],"sourcesContent":["/**\n * Handovers open where the person was reading (`@athenaintel/agent-runtime`'s\n * `handoff-cursor`), for every host of `ChatThreadShell` (Athena in Chrome,\n * the desktop app, `AthenaChat`).\n *\n * As a source, `useReadThreadReadingCursor` reads the shell's viewport at the\n * moment of the click: `null` at the bottom, else the id of the last message\n * fully in view. As a destination, `HandoffCursorReveal` (mounted by the shell\n * for its `arrivalThreadId`) scrolls to the message the arrival names once the\n * transcript has it, paging older history in when it is further back, and\n * highlights it briefly; a cursor it can't find leaves the view at the bottom.\n */\nimport {\n type ThreadMessage,\n type ThreadViewportState,\n useAui,\n useThreadViewport,\n useThreadViewportStore,\n} from \"@assistant-ui/react\";\nimport {\n deepAgentHistoryHasOlder,\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n HANDOFF_CURSOR_SETTLE_MS,\n handoffCursorMessageId,\n handoffCursorRevealStep,\n loadOlderDeepAgentHistoryPage,\n MESSAGE_ROW_ID_ATTRIBUTE,\n readDeepAgentStatewireExtras,\n readingCursorOf,\n settleHandoffArrivalCursor,\n useHandoffArrivalCursor,\n} from \"@athenaintel/agent-runtime\";\nimport type { FC } from \"react\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { THREAD_VIEWPORT_ATTR, THREAD_VIEWPORT_FOOTER_ATTR } from \"./statewire-history-edge\";\n/** What assistant-ui's `ThreadPrimitive.Root` stamps on the thread panel. */\nconst THREAD_ROOT_SELECTOR = \"[data-aui-thread-root]\";\n/** Set on the revealed message while it is highlighted. */\nexport const HANDOFF_CURSOR_REVEALED_ATTR = \"data-handoff-cursor-revealed\";\n/** How often a landing looks for its message while the transcript loads. */\nexport const HANDOFF_CURSOR_POLL_MS = 150;\n\nconst escapeAttributeValue = (value: string): string =>\n typeof CSS !== \"undefined\" && typeof CSS.escape === \"function\"\n ? CSS.escape(value)\n : value.replace(/[\"\\\\]/g, \"\\\\$&\");\n\n/** The thread viewport `anchor` sits in, or the one of the thread panel it sits in. */\nexport function findThreadViewport(anchor: Element | null | undefined): HTMLElement | null {\n if (!anchor) return null;\n const inside = anchor.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`);\n if (inside) return inside;\n return (\n anchor.closest(THREAD_ROOT_SELECTOR)?.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ??\n null\n );\n}\n\n/** How much of `viewport` its sticky footer (the composer dock) covers at the bottom. */\nfunction coveredBottomOf(viewport: HTMLElement): number {\n const footer = viewport.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_FOOTER_ATTR}]`);\n if (!footer) return 0;\n return Math.max(0, viewport.getBoundingClientRect().bottom - footer.getBoundingClientRect().top);\n}\n\n/** Where `viewport` is being read: `null` at the bottom, else a message id. */\nexport function readThreadReadingCursor(viewport: HTMLElement | null): string | null {\n if (!viewport) return null;\n return readingCursorOf({ viewport, coveredBottomPx: coveredBottomOf(viewport) });\n}\n\n/**\n * Reads where the thread is being read, at the moment of a click: inside the\n * viewport (a composer toolbar, even from a portaled dialog) through the\n * thread's viewport store; elsewhere in the panel (a header menu) from the\n * DOM around `anchor`. `null` at the bottom or with no viewport found.\n */\nexport function useReadThreadReadingCursor(): (anchor?: Element | null) => string | null {\n const store = useThreadViewportStore({ optional: true });\n return useCallback(\n (anchor?: Element | null) =>\n readThreadReadingCursor(store?.getState().element.viewport ?? findThreadViewport(anchor)),\n [store],\n );\n}\n\n/** Scrolls `element` into `viewport`'s visible part: centred, or its top when taller. */\nfunction scrollMessageIntoView({\n viewport,\n element,\n}: {\n viewport: HTMLElement;\n element: HTMLElement;\n}): void {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n const visible = Math.max(1, box.height - coveredBottomOf(viewport));\n const offsetTop = rect.top - box.top + viewport.scrollTop;\n const top =\n rect.height >= visible ? offsetTop - 8 : offsetTop - (visible - rect.height) / 2;\n // `instant`: the shell's viewport scrolls smoothly by default.\n viewport.scrollTo({ top: Math.max(0, top), behavior: \"instant\" });\n}\n\nfunction isInView({ viewport, element }: { viewport: HTMLElement; element: HTMLElement }): boolean {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n return rect.bottom > box.top && rect.top < box.bottom - coveredBottomOf(viewport);\n}\n\n/**\n * A brief tint on the revealed message, through the Web Animations API so it\n * needs nothing from the host's stylesheet. Returns its undo.\n */\nfunction highlightMessage(element: HTMLElement): () => void {\n element.setAttribute(HANDOFF_CURSOR_REVEALED_ATTR, \"\");\n const animation =\n typeof element.animate === \"function\"\n ? element.animate(\n [\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n offset: 0.5,\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0)\",\n borderRadius: \"12px\",\n },\n ],\n { duration: HANDOFF_CURSOR_HIGHLIGHT_MS, easing: \"ease-out\" },\n )\n : null;\n const timer = setTimeout(\n () => element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR),\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n );\n return () => {\n clearTimeout(timer);\n animation?.cancel();\n element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR);\n };\n}\n\ninterface ThreadSnapshot {\n messages: readonly ThreadMessage[];\n isLoading: boolean;\n extras: unknown;\n}\n\n/**\n * Opens the transcript at `cursor`: looks for the message every\n * {@link HANDOFF_CURSOR_POLL_MS}, pages older history in while it is further\n * back, scrolls to it, holds it in view while the transcript settles (until\n * the person scrolls), and highlights it. Calls `onSettled` once it revealed\n * the message or gave up (the bottom). Returns its cancel.\n */\nexport function startHandoffCursorReveal({\n cursor,\n readThread,\n readViewport,\n onSettled,\n now = () => Date.now(),\n}: {\n cursor: string;\n readThread: () => ThreadSnapshot;\n readViewport: () => HTMLElement | null;\n onSettled: () => void;\n now?: () => number;\n}): () => void {\n // Progress restarts the wait: the reveal started, or a page of older history landed.\n let waitingSince = now();\n let pagesLoaded = 0;\n let paging = false;\n // A page that landed renders on a later commit: look again before paging on.\n let pageLandsAt = 0;\n let settled = false;\n let revealed: { element: HTMLElement; viewport: HTMLElement; at: number } | null = null;\n let personScrolled = false;\n let unhighlight: (() => void) | null = null;\n let detach: (() => void) | null = null;\n\n const settle = () => {\n if (settled) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n onSettled();\n };\n const pageOlder = () => {\n paging = true;\n loadOlderDeepAgentHistoryPage({\n readExtras: () => readDeepAgentStatewireExtras(readThread().extras),\n })\n .then((loaded) => {\n if (!loaded) {\n // Nothing older could be stated: the history is exhausted.\n pagesLoaded = Number.POSITIVE_INFINITY;\n return;\n }\n pagesLoaded += 1;\n waitingSince = now();\n })\n .catch(() => {\n pagesLoaded = Number.POSITIVE_INFINITY;\n })\n .finally(() => {\n paging = false;\n pageLandsAt = now() + 2 * HANDOFF_CURSOR_POLL_MS;\n });\n };\n const tick = () => {\n if (settled) return;\n if (revealed) {\n if (personScrolled || now() - revealed.at >= HANDOFF_CURSOR_SETTLE_MS) {\n settle();\n return;\n }\n if (revealed.element.isConnected && !isInView(revealed)) scrollMessageIntoView(revealed);\n return;\n }\n const viewport = readViewport();\n const { messages, isLoading, extras } = readThread();\n const targetId = handoffCursorMessageId({ messages, cursor });\n const element =\n targetId !== null && viewport\n ? viewport.querySelector<HTMLElement>(\n `[${MESSAGE_ROW_ID_ATTRIBUTE}=\"${escapeAttributeValue(targetId)}\"]`,\n )\n : null;\n const step = handoffCursorRevealStep({\n found: element !== null,\n // Still attaching, a page in flight, or the row not rendered yet.\n loading:\n isLoading ||\n paging ||\n now() < pageLandsAt ||\n messages.length === 0 ||\n (targetId !== null && !element),\n hasOlder: deepAgentHistoryHasOlder(extras),\n pagesLoaded,\n elapsedMs: now() - waitingSince,\n });\n switch (step) {\n case \"reveal\": {\n if (!viewport || !element) return;\n scrollMessageIntoView({ viewport, element });\n unhighlight = highlightMessage(element);\n revealed = { element, viewport, at: now() };\n const stop = () => {\n personScrolled = true;\n };\n const events = [\"wheel\", \"touchstart\", \"pointerdown\", \"keydown\"] as const;\n for (const event of events) viewport.addEventListener(event, stop, { passive: true });\n detach = () => {\n for (const event of events) viewport.removeEventListener(event, stop);\n };\n return;\n }\n case \"page\":\n pageOlder();\n return;\n case \"wait\":\n return;\n case \"bottom\":\n settle();\n return;\n }\n };\n const timer = setInterval(tick, HANDOFF_CURSOR_POLL_MS);\n tick();\n return () => {\n // Once settled, the highlight fades out on its own timer.\n if (settled) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n unhighlight?.();\n };\n}\n\n/**\n * Opens `threadId`'s transcript at the message the source was reading, once,\n * when this surface picked the conversation up with a cursor\n * (`recordHandoffArrival({ cursor })`). Renders nothing.\n */\nexport const HandoffCursorReveal: FC<{ threadId: string | null | undefined }> = ({ threadId }) => {\n const cursor = useHandoffArrivalCursor(threadId);\n const aui = useAui();\n const viewport = useThreadViewport({\n optional: true,\n selector: (state: ThreadViewportState) => state.element.viewport,\n });\n const viewportRef = useRef(viewport);\n viewportRef.current = viewport;\n useEffect(() => {\n if (!threadId || !cursor) return undefined;\n return startHandoffCursorReveal({\n cursor,\n readThread: () => {\n const state = aui.thread().getState();\n return { messages: state.messages, isLoading: state.isLoading, extras: state.extras };\n },\n readViewport: () => viewportRef.current ?? null,\n onSettled: () => settleHandoffArrivalCursor(threadId),\n });\n }, [aui, cursor, threadId]);\n return null;\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handoff-cursor.js","sources":["../../../src/chat-ui/thread/handoff-cursor.tsx"],"sourcesContent":["/**\n * Handovers open where the person was reading (`@athenaintel/agent-runtime`'s\n * `handoff-cursor`), for every host of `ChatThreadShell` (Athena in Chrome,\n * the desktop app, `AthenaChat`).\n *\n * As a source, `useReadThreadReadingCursor` reads the shell's viewport at the\n * moment of the click: `null` at the bottom, else the id of the last message\n * fully in view. As a destination, `HandoffCursorReveal` (mounted by the shell\n * for its `arrivalThreadId`) scrolls to the message the arrival names once the\n * transcript has it, paging older history in when it is further back, and\n * highlights it briefly; a cursor it can't find leaves the view at the bottom.\n */\nimport {\n type ThreadMessage,\n type ThreadViewportState,\n useAui,\n useThreadViewport,\n useThreadViewportStore,\n} from \"@assistant-ui/react\";\nimport {\n deepAgentHistoryHasOlder,\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n HANDOFF_CURSOR_SETTLE_MS,\n handoffCursorMessageId,\n handoffCursorRevealStep,\n isHistoryPageLostToReconnect,\n loadOlderDeepAgentHistoryPage,\n MESSAGE_ROW_ID_ATTRIBUTE,\n readDeepAgentStatewireExtras,\n readHandoffArrival,\n readingCursorOf,\n settleHandoffArrivalCursor,\n useHandoffArrivalCursor,\n} from \"@athenaintel/agent-runtime\";\nimport type { FC } from \"react\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { THREAD_VIEWPORT_ATTR, THREAD_VIEWPORT_FOOTER_ATTR } from \"./statewire-history-edge\";\n/** What assistant-ui's `ThreadPrimitive.Root` stamps on the thread panel. */\nconst THREAD_ROOT_SELECTOR = \"[data-aui-thread-root]\";\n/** Set on the revealed message while it is highlighted. */\nexport const HANDOFF_CURSOR_REVEALED_ATTR = \"data-handoff-cursor-revealed\";\n/** How often a landing looks for its message while the transcript loads. */\nexport const HANDOFF_CURSOR_POLL_MS = 150;\n\nconst escapeAttributeValue = (value: string): string =>\n typeof CSS !== \"undefined\" && typeof CSS.escape === \"function\"\n ? CSS.escape(value)\n : value.replace(/[\"\\\\]/g, \"\\\\$&\");\n\n/** The thread viewport `anchor` sits in, or the one of the thread panel it sits in. */\nexport function findThreadViewport(anchor: Element | null | undefined): HTMLElement | null {\n if (!anchor) return null;\n const inside = anchor.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`);\n if (inside) return inside;\n return (\n anchor.closest(THREAD_ROOT_SELECTOR)?.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ??\n null\n );\n}\n\n/** How much of `viewport` its sticky footer (the composer dock) covers at the bottom. */\nfunction coveredBottomOf(viewport: HTMLElement): number {\n const footer = viewport.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_FOOTER_ATTR}]`);\n if (!footer) return 0;\n return Math.max(0, viewport.getBoundingClientRect().bottom - footer.getBoundingClientRect().top);\n}\n\n/** Where `viewport` is being read: `null` at the bottom, else a message id. */\nexport function readThreadReadingCursor(viewport: HTMLElement | null): string | null {\n if (!viewport) return null;\n return readingCursorOf({ viewport, coveredBottomPx: coveredBottomOf(viewport) });\n}\n\n/**\n * Reads where the thread is being read, at the moment of a click: inside the\n * viewport (a composer toolbar, even from a portaled dialog) through the\n * thread's viewport store; elsewhere in the panel (a header menu) from the\n * DOM around `anchor`. `null` at the bottom or with no viewport found.\n */\nexport function useReadThreadReadingCursor(): (anchor?: Element | null) => string | null {\n const store = useThreadViewportStore({ optional: true });\n return useCallback(\n (anchor?: Element | null) =>\n readThreadReadingCursor(store?.getState().element.viewport ?? findThreadViewport(anchor)),\n [store],\n );\n}\n\n/** Scrolls `element` into `viewport`'s visible part: centred, or its top when taller. */\nfunction scrollMessageIntoView({\n viewport,\n element,\n}: {\n viewport: HTMLElement;\n element: HTMLElement;\n}): void {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n const visible = Math.max(1, box.height - coveredBottomOf(viewport));\n const offsetTop = rect.top - box.top + viewport.scrollTop;\n const top =\n rect.height >= visible ? offsetTop - 8 : offsetTop - (visible - rect.height) / 2;\n // `instant`: the shell's viewport scrolls smoothly by default.\n viewport.scrollTo({ top: Math.max(0, top), behavior: \"instant\" });\n}\n\nfunction isInView({ viewport, element }: { viewport: HTMLElement; element: HTMLElement }): boolean {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n return rect.bottom > box.top && rect.top < box.bottom - coveredBottomOf(viewport);\n}\n\n/**\n * A brief tint on the revealed message, through the Web Animations API so it\n * needs nothing from the host's stylesheet. Returns its undo.\n */\nfunction highlightMessage(element: HTMLElement): () => void {\n element.setAttribute(HANDOFF_CURSOR_REVEALED_ATTR, \"\");\n const animation =\n typeof element.animate === \"function\"\n ? element.animate(\n [\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n offset: 0.5,\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0)\",\n borderRadius: \"12px\",\n },\n ],\n { duration: HANDOFF_CURSOR_HIGHLIGHT_MS, easing: \"ease-out\" },\n )\n : null;\n const timer = setTimeout(\n () => element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR),\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n );\n return () => {\n clearTimeout(timer);\n animation?.cancel();\n element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR);\n };\n}\n\ninterface ThreadSnapshot {\n messages: readonly ThreadMessage[];\n isLoading: boolean;\n extras: unknown;\n}\n\n/**\n * Opens the transcript at `cursor`: looks for the message every\n * {@link HANDOFF_CURSOR_POLL_MS}, pages older history in while it is further\n * back, scrolls to it, holds it in view while the transcript settles (until\n * the person scrolls), and highlights it. `claim` takes the pickup right\n * before the scroll (`false`: another transcript showing the conversation took\n * it, and this one stays put). Calls `onSettled` once it gave up (the bottom)\n * or its hold ended. Returns its cancel, which leaves a revealed message's\n * hold and highlight to finish on their own unless `endHold` (a newer pickup\n * took over).\n */\nexport function startHandoffCursorReveal({\n cursor,\n readThread,\n readViewport,\n claim = () => true,\n onSettled,\n now = () => Date.now(),\n}: {\n cursor: string;\n readThread: () => ThreadSnapshot;\n readViewport: () => HTMLElement | null;\n claim?: () => boolean;\n onSettled: () => void;\n now?: () => number;\n}): (options?: { endHold?: boolean }) => void {\n // Progress restarts the wait: the reveal started, or a page of older history landed.\n let waitingSince = now();\n let pagesLoaded = 0;\n let paging = false;\n // A page that landed renders on a later commit: look again before paging on.\n let pageLandsAt = 0;\n let settled = false;\n let revealed: { element: HTMLElement; viewport: HTMLElement; at: number } | null = null;\n let personScrolled = false;\n let unhighlight: (() => void) | null = null;\n let detach: (() => void) | null = null;\n\n const settle = () => {\n if (settled) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n onSettled();\n };\n const pageOlder = () => {\n paging = true;\n loadOlderDeepAgentHistoryPage({\n readExtras: () => readDeepAgentStatewireExtras(readThread().extras),\n })\n .then((loaded) => {\n if (!loaded) {\n // Nothing older could be stated: the history is exhausted.\n pagesLoaded = Number.POSITIVE_INFINITY;\n return;\n }\n pagesLoaded += 1;\n waitingSince = now();\n })\n .catch((error: unknown) => {\n // A dropped connection is not the end of the history: the re-attach\n // carries what was loaded, and the next look pages on from there.\n if (!isHistoryPageLostToReconnect(error)) pagesLoaded = Number.POSITIVE_INFINITY;\n })\n .finally(() => {\n paging = false;\n pageLandsAt = now() + 2 * HANDOFF_CURSOR_POLL_MS;\n });\n };\n const tick = () => {\n if (settled) return;\n if (revealed) {\n if (personScrolled || now() - revealed.at >= HANDOFF_CURSOR_SETTLE_MS) {\n settle();\n return;\n }\n if (revealed.element.isConnected && !isInView(revealed)) scrollMessageIntoView(revealed);\n return;\n }\n const viewport = readViewport();\n const { messages, isLoading, extras } = readThread();\n const targetId = handoffCursorMessageId({ messages, cursor });\n const element =\n targetId !== null && viewport\n ? viewport.querySelector<HTMLElement>(\n `[${MESSAGE_ROW_ID_ATTRIBUTE}=\"${escapeAttributeValue(targetId)}\"]`,\n )\n : null;\n const step = handoffCursorRevealStep({\n found: element !== null,\n // Still attaching, a page in flight, or the row not rendered yet.\n loading:\n isLoading ||\n paging ||\n now() < pageLandsAt ||\n messages.length === 0 ||\n (targetId !== null && !element),\n hasOlder: deepAgentHistoryHasOlder(extras),\n pagesLoaded,\n elapsedMs: now() - waitingSince,\n });\n switch (step) {\n case \"reveal\": {\n if (!viewport || !element) return;\n if (!claim()) {\n // Another transcript showing this conversation took the pickup.\n settled = true;\n clearInterval(timer);\n return;\n }\n scrollMessageIntoView({ viewport, element });\n unhighlight = highlightMessage(element);\n revealed = { element, viewport, at: now() };\n const stop = () => {\n personScrolled = true;\n };\n const events = [\"wheel\", \"touchstart\", \"pointerdown\", \"keydown\"] as const;\n for (const event of events) viewport.addEventListener(event, stop, { passive: true });\n detach = () => {\n for (const event of events) viewport.removeEventListener(event, stop);\n };\n return;\n }\n case \"page\":\n pageOlder();\n return;\n case \"wait\":\n return;\n case \"bottom\":\n settle();\n return;\n }\n };\n const timer = setInterval(tick, HANDOFF_CURSOR_POLL_MS);\n tick();\n return ({ endHold = false } = {}) => {\n if (endHold) unhighlight?.();\n if (settled) return;\n // A revealed message's hold finishes on its own timer, unless a newer pickup took over.\n if (revealed && !endHold) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n unhighlight?.();\n };\n}\n\n/**\n * Opens `threadId`'s transcript at the message the source was reading, once,\n * when this surface picked the conversation up with a cursor\n * (`recordHandoffArrival({ cursor })`). When two transcripts show the\n * conversation, the first to find the message owns the pickup. Renders nothing.\n */\nexport const HandoffCursorReveal: FC<{ threadId: string | null | undefined }> = ({ threadId }) => {\n const cursor = useHandoffArrivalCursor(threadId);\n const aui = useAui();\n const viewport = useThreadViewport({\n optional: true,\n selector: (state: ThreadViewportState) => state.element.viewport,\n });\n const viewportRef = useRef(viewport);\n viewportRef.current = viewport;\n // The latest pickup's reveal: its hold outlives the claim that settles its cursor.\n const latestRef = useRef<ReturnType<typeof startHandoffCursorReveal> | null>(null);\n useEffect(() => {\n if (!threadId || !cursor) return undefined;\n // A newer pickup takes over: the previous message stops being held and highlighted.\n latestRef.current?.({ endHold: true });\n const cancel = startHandoffCursorReveal({\n cursor,\n readThread: () => {\n const state = aui.thread().getState();\n return { messages: state.messages, isLoading: state.isLoading, extras: state.extras };\n },\n readViewport: () => viewportRef.current ?? null,\n // Settled before the scroll, so a second transcript's reveal stops at its claim.\n claim: () => {\n if (readHandoffArrival(threadId)?.cursor !== cursor) return false;\n settleHandoffArrivalCursor(threadId);\n return true;\n },\n // Only this pickup's: a revealed message's hold ends after its claim\n // settled the cursor, when a later handover may have recorded another.\n onSettled: () => {\n if (readHandoffArrival(threadId)?.cursor === cursor) settleHandoffArrivalCursor(threadId);\n },\n });\n latestRef.current = cancel;\n return cancel;\n }, [aui, cursor, threadId]);\n return null;\n};\n"],"names":["_a"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const initialSync = require("../keryx-http/src/initial-sync.cjs");
3
4
  const receiveOnlySocket = require("../keryx-http/src/receive-only-socket.cjs");
4
5
  const unconfirmedUpdateAttributions = require("../keryx-http/src/unconfirmed-update-attributions.cjs");
5
6
  const updateAttributions = require("../keryx-http/src/update-attributions.cjs");
@@ -53,6 +54,14 @@ function socketFor({
53
54
  unconfirmed
54
55
  });
55
56
  }
57
+ const HTTP_FIRST_READ_TIMEOUT_MS = 6e4;
58
+ function browserStorage() {
59
+ try {
60
+ return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
56
65
  function connectGenericDoc(args) {
57
66
  const access = args.access ?? "view";
58
67
  const mint2 = resolveMint({ auth: args.auth, mint: args.mint });
@@ -91,6 +100,8 @@ function connectGenericDoc(args) {
91
100
  let localChangesSynced = true;
92
101
  let liveGrant = null;
93
102
  let readStatusOnConnect = false;
103
+ let keryxAnswersSyncStep1 = false;
104
+ let httpFirstRead = null;
94
105
  const notify = () => {
95
106
  version += 1;
96
107
  for (const listener of listeners) listener();
@@ -178,6 +189,10 @@ function connectGenericDoc(args) {
178
189
  if (provider) {
179
190
  const previous = provider;
180
191
  provider = null;
192
+ if ((httpFirstRead == null ? void 0 : httpFirstRead.target) === previous) {
193
+ httpFirstRead.controller.abort(new DOMException("The room closed", "AbortError"));
194
+ httpFirstRead = null;
195
+ }
181
196
  carriedUpdates = [...carriedUpdates, ...previous.unconfirmedUpdates];
182
197
  previous.destroy();
183
198
  }
@@ -210,7 +225,7 @@ function connectGenericDoc(args) {
210
225
  offlineTimer = setTimeout(() => {
211
226
  offlineTimer = null;
212
227
  if (destroyed || !connectionEnabled || status === "connected" || provider === null) return;
213
- if (provider.wsconnecting && !hasUnsyncedLocalEdits()) {
228
+ if ((provider.wsconnecting || (httpFirstRead == null ? void 0 : httpFirstRead.target) === provider) && !hasUnsyncedLocalEdits()) {
214
229
  openingPastThreshold = provider;
215
230
  armOfflineTakeover();
216
231
  return;
@@ -291,16 +306,23 @@ function connectGenericDoc(args) {
291
306
  }
292
307
  if (destroyed || !connectionEnabled || seq !== connectSeq) return;
293
308
  const socket = socketFor({ grant, doc, unconfirmed: unconfirmedAttributions });
309
+ const roomUrl = `${grant.ws_url}/${grant.org}/${grant.doc_id}`;
310
+ const httpFirstUrl = args.httpFirstOpen === true && doc.store.clients.size === 0 && carriedUpdates.length === 0 ? initialSync.keryxRoomReadUrl({ wsUrl: roomUrl, token: grant.token, branch: grant.branch }) : null;
311
+ const params = {
312
+ yauth: grant.token,
313
+ branch: grant.branch,
314
+ gc: "true"
315
+ };
316
+ if (httpFirstUrl === null && asksForDiffedSync({ roomUrl, unconfirmed: carriedUpdates.length })) {
317
+ params[initialSync.INITIAL_SYNC_PARAM] = initialSync.INITIAL_SYNC_DIFF;
318
+ }
294
319
  const nextProvider = new websocket.WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {
295
- params: {
296
- yauth: grant.token,
297
- branch: grant.branch,
298
- gc: "true"
299
- },
320
+ params,
300
321
  // BroadcastChannel would sync same-origin clients locally, bypassing the
301
322
  // server's permission enforcement (a read-only tab could write into an
302
323
  // editor tab's doc). Permissioned docs must round-trip the server.
303
324
  disableBc: true,
325
+ connect: httpFirstUrl === null,
304
326
  ...socket && { WebSocketPolyfill: socket }
305
327
  });
306
328
  provider = nextProvider;
@@ -342,6 +364,11 @@ function connectGenericDoc(args) {
342
364
  });
343
365
  nextProvider.on("connection-close", (event) => {
344
366
  if (provider !== nextProvider) return;
367
+ if (asksForDiffedSync({ roomUrl, unconfirmed: nextProvider.unconfirmedUpdates.length })) {
368
+ nextProvider.params[initialSync.INITIAL_SYNC_PARAM] = initialSync.INITIAL_SYNC_DIFF;
369
+ } else {
370
+ delete nextProvider.params[initialSync.INITIAL_SYNC_PARAM];
371
+ }
345
372
  const action = closePolicy.classifyClose(event == null ? void 0 : event.code);
346
373
  if (action === "remint") {
347
374
  consecutiveRemints += 1;
@@ -369,7 +396,68 @@ function connectGenericDoc(args) {
369
396
  notify();
370
397
  });
371
398
  scheduleRefresh(grant);
399
+ if (httpFirstUrl !== null) void openOverHttp({ target: nextProvider, url: httpFirstUrl, roomUrl });
372
400
  };
401
+ function asksForDiffedSync({ roomUrl, unconfirmed }) {
402
+ if (args.httpFirstOpen !== true) return false;
403
+ const origin = initialSync.keryxOriginOf(roomUrl);
404
+ const known = keryxAnswersSyncStep1 || origin !== null && initialSync.knownInitialSyncSupport({ storage: browserStorage(), origin }) === true;
405
+ return known && doc.store.clients.size > 0 && unconfirmed === 0 && !unconfirmedAttributions.keepsWrites;
406
+ }
407
+ async function openOverHttp({
408
+ target,
409
+ url,
410
+ roomUrl
411
+ }) {
412
+ var _a;
413
+ const started = performance.now();
414
+ const origin = initialSync.keryxOriginOf(roomUrl);
415
+ const storage = browserStorage();
416
+ let event;
417
+ if (origin !== null && initialSync.knownInitialSyncSupport({ storage, origin }) === false) {
418
+ event = { outcome: "skipped", durationMs: 0 };
419
+ } else {
420
+ const controller = new AbortController();
421
+ httpFirstRead = { target, controller };
422
+ const timeout = setTimeout(
423
+ () => controller.abort(new DOMException("The HTTP-first read timed out", "TimeoutError")),
424
+ HTTP_FIRST_READ_TIMEOUT_MS
425
+ );
426
+ try {
427
+ const read = await initialSync.readRoomOverHttp({ url, signal: controller.signal });
428
+ if (origin !== null) initialSync.rememberInitialSyncSupport({ storage, origin, supported: read.answersSyncStep1 });
429
+ if (read.answersSyncStep1 && provider === target && !destroyed) {
430
+ if (read.update.byteLength > 2) Y__namespace.applyUpdate(doc, read.update, target);
431
+ keryxAnswersSyncStep1 = true;
432
+ }
433
+ event = {
434
+ outcome: read.answersSyncStep1 ? "read" : "unsupported",
435
+ bytes: read.bytes,
436
+ durationMs: performance.now() - started
437
+ };
438
+ } catch (err) {
439
+ event = controller.signal.aborted && (destroyed || provider !== target) ? { outcome: "aborted", durationMs: performance.now() - started } : {
440
+ outcome: "failed",
441
+ durationMs: performance.now() - started,
442
+ error: err instanceof Error ? `${err.name}: ${err.message}` : String(err)
443
+ };
444
+ } finally {
445
+ clearTimeout(timeout);
446
+ if ((httpFirstRead == null ? void 0 : httpFirstRead.controller) === controller) httpFirstRead = null;
447
+ }
448
+ }
449
+ try {
450
+ (_a = args.onHttpFirstOpen) == null ? void 0 : _a.call(args, event);
451
+ } catch {
452
+ }
453
+ if (destroyed || provider !== target) return;
454
+ if (asksForDiffedSync({ roomUrl, unconfirmed: target.unconfirmedUpdates.length })) {
455
+ target.params[initialSync.INITIAL_SYNC_PARAM] = initialSync.INITIAL_SYNC_DIFF;
456
+ } else {
457
+ delete target.params[initialSync.INITIAL_SYNC_PARAM];
458
+ }
459
+ target.connect();
460
+ }
373
461
  const preparing = localCopy ? localCopy.prepare({ doc, transport }).catch(() => ({ unsyncedEdits: null })).then(({ unsyncedEdits }) => {
374
462
  if (destroyed) return;
375
463
  if (unsyncedEdits && !(unsyncedEdits.inserts.isEmpty() && unsyncedEdits.deletes.isEmpty())) {