@jskit-ai/assistant-core 0.1.165 → 0.1.166

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 (63) hide show
  1. package/examples/conversation/App.vue +104 -0
  2. package/examples/conversation/README.md +42 -0
  3. package/examples/conversation/dist/assets/index-CSTXm6q3.css +1 -0
  4. package/examples/conversation/dist/assets/index-DGgOLzoo.js +30 -0
  5. package/examples/conversation/dist/index.html +4 -0
  6. package/examples/conversation/index.html +2 -0
  7. package/examples/conversation/main.js +9 -0
  8. package/examples/conversation/node_modules/.vite/deps/@mdi_js.js +7450 -0
  9. package/examples/conversation/node_modules/.vite/deps/@mdi_js.js.map +1 -0
  10. package/examples/conversation/node_modules/.vite/deps/VTooltip-jIxO6ULs.js +2311 -0
  11. package/examples/conversation/node_modules/.vite/deps/VTooltip-jIxO6ULs.js.map +1 -0
  12. package/examples/conversation/node_modules/.vite/deps/_metadata.json +70 -0
  13. package/examples/conversation/node_modules/.vite/deps/defineComponent-DK6LSjDW.js +710 -0
  14. package/examples/conversation/node_modules/.vite/deps/defineComponent-DK6LSjDW.js.map +1 -0
  15. package/examples/conversation/node_modules/.vite/deps/hotkey-parsing-DnoZxtbW.js +1214 -0
  16. package/examples/conversation/node_modules/.vite/deps/hotkey-parsing-DnoZxtbW.js.map +1 -0
  17. package/examples/conversation/node_modules/.vite/deps/icons-ze2hmg7j.js +98 -0
  18. package/examples/conversation/node_modules/.vite/deps/icons-ze2hmg7j.js.map +1 -0
  19. package/examples/conversation/node_modules/.vite/deps/package.json +3 -0
  20. package/examples/conversation/node_modules/.vite/deps/theme-D78LVcJg.js +1301 -0
  21. package/examples/conversation/node_modules/.vite/deps/theme-D78LVcJg.js.map +1 -0
  22. package/examples/conversation/node_modules/.vite/deps/vue.js +2 -0
  23. package/examples/conversation/node_modules/.vite/deps/vue.runtime.esm-bundler-YcakWsDn.js +8538 -0
  24. package/examples/conversation/node_modules/.vite/deps/vue.runtime.esm-bundler-YcakWsDn.js.map +1 -0
  25. package/examples/conversation/node_modules/.vite/deps/vuetify.js +413 -0
  26. package/examples/conversation/node_modules/.vite/deps/vuetify.js.map +1 -0
  27. package/examples/conversation/node_modules/.vite/deps/vuetify_components.js +25370 -0
  28. package/examples/conversation/node_modules/.vite/deps/vuetify_components.js.map +1 -0
  29. package/examples/conversation/node_modules/.vite/deps/vuetify_directives.js +132 -0
  30. package/examples/conversation/node_modules/.vite/deps/vuetify_directives.js.map +1 -0
  31. package/examples/conversation/node_modules/.vite/deps/vuetify_iconsets_mdi-svg.js +70 -0
  32. package/examples/conversation/node_modules/.vite/deps/vuetify_iconsets_mdi-svg.js.map +1 -0
  33. package/examples/conversation/package.json +22 -0
  34. package/examples/conversation/server.js +123 -0
  35. package/examples/conversation/vite.config.js +8 -0
  36. package/package.json +59 -7
  37. package/src/client/conversation/AssistantComposerActions.vue +43 -0
  38. package/src/client/conversation/AssistantConversationElement.vue +78 -0
  39. package/src/client/conversation/AssistantProgress.vue +88 -0
  40. package/src/client/conversation/AssistantPromptInput.vue +467 -0
  41. package/src/client/conversation/AssistantTranscript.vue +1361 -0
  42. package/src/client/conversation/LongTextInlineParts.vue +64 -0
  43. package/src/client/conversation/LongTextPreviewBlocks.vue +324 -0
  44. package/src/client/conversation/index.js +9 -0
  45. package/src/client/conversation/submitText.js +29 -0
  46. package/src/client/conversation/useScrollToBottom.js +151 -0
  47. package/src/server/conversation/codexClient.js +328 -0
  48. package/src/server/conversation/codexEvents.js +623 -0
  49. package/src/server/conversation/codexTurn.js +280 -0
  50. package/src/server/conversation/index.js +2 -0
  51. package/src/server/conversation/memoryStorage.js +76 -0
  52. package/src/server/conversation/normalize.js +7 -0
  53. package/src/server/conversation/openCodeClient.js +512 -0
  54. package/src/server/conversation/transcript.js +92 -0
  55. package/src/shared/conversation/answerChoiceSugar.js +191 -0
  56. package/src/shared/conversation/index.js +6 -0
  57. package/src/shared/conversation/longTextBlocks.js +403 -0
  58. package/src/shared/conversation/numberedQuestionSugar.js +295 -0
  59. package/src/shared/conversation/scrollFollowState.js +32 -0
  60. package/src/shared/conversation/thinkingText.js +13 -0
  61. package/src/shared/conversation/turns.js +30 -0
  62. package/src/testing/conversationStorageContract.js +35 -0
  63. package/test/conversation.test.js +57 -0
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <template
3
+ v-for="(part, partIndex) in parts"
4
+ :key="partKey(part, partIndex)"
5
+ >
6
+ <strong v-if="part.type === 'strong'">{{ part.text }}</strong>
7
+ <code v-else-if="part.type === 'code'">{{ part.text }}</code>
8
+ <a
9
+ v-else-if="part.type === 'link'"
10
+ :href="part.href"
11
+ @click="handleLinkClick($event, part)"
12
+ >{{ part.text }}</a>
13
+ <span v-else>{{ part.text }}</span>
14
+ </template>
15
+ </template>
16
+
17
+ <script setup>
18
+ import { computed } from "vue";
19
+
20
+ import { parseLongTextInlineParts } from "../../shared/conversation/longTextBlocks.js";
21
+
22
+ const props = defineProps({
23
+ text: {
24
+ default: "",
25
+ type: String
26
+ }
27
+ });
28
+ const emit = defineEmits(["link-click"]);
29
+
30
+ const parts = computed(() => parseLongTextInlineParts(props.text));
31
+
32
+ function partKey(part, partIndex) {
33
+ return `${part.type}:${partIndex}:${part.text}`;
34
+ }
35
+
36
+ function handleLinkClick(event, part = {}) {
37
+ emit("link-click", {
38
+ event,
39
+ href: part.href || "",
40
+ text: part.text || ""
41
+ });
42
+ }
43
+ </script>
44
+
45
+ <style scoped>
46
+ code {
47
+ background: rgba(var(--v-theme-primary), 0.07);
48
+ border: 1px solid rgba(var(--v-theme-primary), 0.14);
49
+ border-radius: 3px;
50
+ color: rgba(var(--v-theme-on-surface), 0.9);
51
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
52
+ font-size: 0.9em;
53
+ overflow-wrap: anywhere;
54
+ padding: 0.02rem 0.2rem;
55
+ word-break: break-word;
56
+ }
57
+
58
+ span,
59
+ strong,
60
+ a {
61
+ overflow-wrap: anywhere;
62
+ word-break: break-word;
63
+ }
64
+ </style>
@@ -0,0 +1,324 @@
1
+ <template>
2
+ <div
3
+ class="studio-long-text-review__blocks"
4
+ :class="{ 'studio-long-text-review__blocks--compact': compact }"
5
+ >
6
+ <template
7
+ v-for="(block, blockIndex) in blocks"
8
+ :key="blockKey(block, blockIndex)"
9
+ >
10
+ <component
11
+ :is="headingTag(block.level)"
12
+ v-if="block.type === 'heading'"
13
+ class="studio-long-text-review__heading"
14
+ >
15
+ <LongTextInlineParts
16
+ :text="block.text"
17
+ @link-click="emit('link-click', $event)"
18
+ />
19
+ </component>
20
+ <component
21
+ :is="block.type"
22
+ v-else-if="isListBlock(block)"
23
+ class="studio-long-text-review__list"
24
+ >
25
+ <li
26
+ v-for="(item, itemIndex) in block.items"
27
+ :key="`item:${blockIndex}:${itemIndex}`"
28
+ >
29
+ <LongTextInlineParts
30
+ :text="item.text"
31
+ @link-click="emit('link-click', $event)"
32
+ />
33
+ </li>
34
+ </component>
35
+ <pre
36
+ v-else-if="block.type === 'code'"
37
+ class="studio-long-text-review__code"
38
+ ><code>{{ block.text }}</code></pre>
39
+ <div
40
+ v-else-if="block.type === 'table'"
41
+ class="studio-long-text-review__table-wrap"
42
+ role="region"
43
+ aria-label="Table"
44
+ tabindex="0"
45
+ >
46
+ <table class="studio-long-text-review__table">
47
+ <thead>
48
+ <tr>
49
+ <th
50
+ v-for="(header, columnIndex) in block.headers"
51
+ :key="`header:${blockIndex}:${columnIndex}`"
52
+ :class="tableCellClass(block, columnIndex)"
53
+ scope="col"
54
+ >
55
+ <LongTextInlineParts
56
+ :text="header"
57
+ @link-click="emit('link-click', $event)"
58
+ />
59
+ </th>
60
+ </tr>
61
+ </thead>
62
+ <tbody>
63
+ <tr
64
+ v-for="(row, rowIndex) in block.rows"
65
+ :key="`row:${blockIndex}:${rowIndex}`"
66
+ >
67
+ <td
68
+ v-for="(cell, columnIndex) in row"
69
+ :key="`cell:${blockIndex}:${rowIndex}:${columnIndex}`"
70
+ :class="tableCellClass(block, columnIndex)"
71
+ >
72
+ <LongTextInlineParts
73
+ :text="cell"
74
+ @link-click="emit('link-click', $event)"
75
+ />
76
+ </td>
77
+ </tr>
78
+ </tbody>
79
+ </table>
80
+ </div>
81
+ <details
82
+ v-else-if="block.type === 'details'"
83
+ class="studio-long-text-review__details"
84
+ >
85
+ <summary class="studio-long-text-review__details-summary">
86
+ <LongTextInlineParts
87
+ :text="block.summary || 'Details'"
88
+ @link-click="emit('link-click', $event)"
89
+ />
90
+ </summary>
91
+ <LongTextPreviewBlocks
92
+ class="studio-long-text-review__details-body"
93
+ compact
94
+ :blocks="block.blocks || []"
95
+ @link-click="emit('link-click', $event)"
96
+ />
97
+ </details>
98
+ <p
99
+ v-else
100
+ class="studio-long-text-review__paragraph"
101
+ >
102
+ <LongTextInlineParts
103
+ :text="block.text"
104
+ @link-click="emit('link-click', $event)"
105
+ />
106
+ </p>
107
+ </template>
108
+ </div>
109
+ </template>
110
+
111
+ <script setup>
112
+ import LongTextInlineParts from "./LongTextInlineParts.vue";
113
+
114
+ defineProps({
115
+ blocks: {
116
+ type: Array,
117
+ required: true
118
+ },
119
+ compact: {
120
+ type: Boolean,
121
+ default: false
122
+ }
123
+ });
124
+ const emit = defineEmits(["link-click"]);
125
+
126
+ function isListBlock(block) {
127
+ return block.type === "ul" || block.type === "ol";
128
+ }
129
+
130
+ function blockKey(block, blockIndex) {
131
+ return `${block.type}:${blockIndex}`;
132
+ }
133
+
134
+ function headingTag(level) {
135
+ if (level <= 1) {
136
+ return "h3";
137
+ }
138
+ if (level === 2) {
139
+ return "h4";
140
+ }
141
+ return "h5";
142
+ }
143
+
144
+ function tableCellClass(block = {}, columnIndex = 0) {
145
+ const alignment = Array.isArray(block.alignments) ? block.alignments[columnIndex] : "";
146
+ return {
147
+ "studio-long-text-review__table-cell--center": alignment === "center",
148
+ "studio-long-text-review__table-cell--right": alignment === "right"
149
+ };
150
+ }
151
+ </script>
152
+
153
+ <style scoped>
154
+ .studio-long-text-review__blocks {
155
+ color: rgb(var(--v-theme-on-surface));
156
+ display: grid;
157
+ gap: 0.36rem;
158
+ line-height: 1.4;
159
+ max-width: 100%;
160
+ min-width: 0;
161
+ overflow-wrap: anywhere;
162
+ word-break: break-word;
163
+ }
164
+
165
+ .studio-long-text-review__blocks--compact {
166
+ gap: 0.24rem;
167
+ line-height: 1.28;
168
+ }
169
+
170
+ .studio-long-text-review__heading {
171
+ font-weight: 760;
172
+ letter-spacing: 0;
173
+ line-height: 1.15;
174
+ margin: 0;
175
+ overflow-wrap: anywhere;
176
+ word-break: break-word;
177
+ }
178
+
179
+ h3.studio-long-text-review__heading {
180
+ font-size: 1.1rem;
181
+ }
182
+
183
+ h4.studio-long-text-review__heading {
184
+ font-size: 0.98rem;
185
+ }
186
+
187
+ h5.studio-long-text-review__heading {
188
+ font-size: 0.9rem;
189
+ }
190
+
191
+ .studio-long-text-review__paragraph {
192
+ font-size: 0.88rem;
193
+ margin: 0;
194
+ overflow-wrap: anywhere;
195
+ word-break: break-word;
196
+ }
197
+
198
+ .studio-long-text-review__blocks--compact .studio-long-text-review__paragraph {
199
+ font-size: 0.8rem;
200
+ }
201
+
202
+ .studio-long-text-review__list {
203
+ display: grid;
204
+ gap: 0.16rem;
205
+ margin: 0;
206
+ min-width: 0;
207
+ overflow-wrap: anywhere;
208
+ padding-inline-start: 1.3rem;
209
+ }
210
+
211
+ .studio-long-text-review__list li {
212
+ font-size: 0.92rem;
213
+ line-height: 1.36;
214
+ min-width: 0;
215
+ overflow-wrap: anywhere;
216
+ padding-inline-start: 0.1rem;
217
+ word-break: break-word;
218
+ }
219
+
220
+ .studio-long-text-review__blocks--compact .studio-long-text-review__list li {
221
+ font-size: 0.8rem;
222
+ }
223
+
224
+ .studio-long-text-review__code {
225
+ background: rgba(var(--v-theme-surface-variant), 0.58);
226
+ border: 1px solid rgba(var(--v-border-color), 0.26);
227
+ border-radius: 8px;
228
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
229
+ font-size: 0.82rem;
230
+ line-height: 1.36;
231
+ margin: 0;
232
+ max-width: 100%;
233
+ min-width: 0;
234
+ overflow: auto;
235
+ padding: 0.46rem 0.55rem;
236
+ white-space: pre;
237
+ }
238
+
239
+ .studio-long-text-review__table-wrap {
240
+ border: 1px solid rgba(var(--v-border-color), 0.28);
241
+ border-radius: 8px;
242
+ inline-size: fit-content;
243
+ margin-block: 0.35rem;
244
+ max-width: 100%;
245
+ min-width: 0;
246
+ overflow: auto;
247
+ }
248
+
249
+ .studio-long-text-review__table-wrap:focus-visible {
250
+ outline: 2px solid rgb(var(--v-theme-primary));
251
+ outline-offset: -2px;
252
+ }
253
+
254
+ .studio-long-text-review__table {
255
+ border-collapse: separate;
256
+ border-spacing: 0;
257
+ font-size: 0.88rem;
258
+ inline-size: auto;
259
+ line-height: 1.45;
260
+ overflow-wrap: break-word;
261
+ word-break: normal;
262
+ }
263
+
264
+ .studio-long-text-review__table th,
265
+ .studio-long-text-review__table td {
266
+ border-bottom: 1px solid rgba(var(--v-border-color), 0.22);
267
+ max-inline-size: 28rem;
268
+ padding: 0.55rem 0.75rem;
269
+ text-align: left;
270
+ vertical-align: top;
271
+ }
272
+
273
+ .studio-long-text-review__table th {
274
+ background: rgba(var(--v-theme-surface-variant), 0.48);
275
+ color: rgb(var(--v-theme-on-surface));
276
+ font-weight: 760;
277
+ position: sticky;
278
+ top: 0;
279
+ z-index: 1;
280
+ }
281
+
282
+ .studio-long-text-review__table tbody tr:last-child td {
283
+ border-bottom: 0;
284
+ }
285
+
286
+ .studio-long-text-review__table :deep(span),
287
+ .studio-long-text-review__table :deep(strong),
288
+ .studio-long-text-review__table :deep(a),
289
+ .studio-long-text-review__table :deep(code) {
290
+ overflow-wrap: break-word;
291
+ word-break: normal;
292
+ }
293
+
294
+ .studio-long-text-review__table-cell--right {
295
+ font-variant-numeric: tabular-nums;
296
+ text-align: right !important;
297
+ }
298
+
299
+ .studio-long-text-review__table-cell--center {
300
+ text-align: center !important;
301
+ }
302
+
303
+ .studio-long-text-review__details {
304
+ border: 1px solid rgba(var(--v-border-color), 0.34);
305
+ border-radius: 8px;
306
+ display: block;
307
+ min-width: 0;
308
+ overflow: hidden;
309
+ }
310
+
311
+ .studio-long-text-review__details-summary {
312
+ background: rgba(var(--v-theme-surface-variant), 0.48);
313
+ cursor: pointer;
314
+ font-size: 0.82rem;
315
+ font-weight: 650;
316
+ line-height: 1.2;
317
+ list-style-position: inside;
318
+ padding: 0.42rem 0.55rem;
319
+ }
320
+
321
+ .studio-long-text-review__details-body {
322
+ padding: 0.5rem 0.6rem 0.58rem;
323
+ }
324
+ </style>
@@ -0,0 +1,9 @@
1
+ export { default as AssistantConversationElement } from "./AssistantConversationElement.vue";
2
+ export { default as AssistantTranscript } from "./AssistantTranscript.vue";
3
+ export { default as AssistantPromptInput } from "./AssistantPromptInput.vue";
4
+ export { default as AssistantComposerActions } from "./AssistantComposerActions.vue";
5
+ export { default as AssistantProgress } from "./AssistantProgress.vue";
6
+ export { default as LongTextPreviewBlocks } from "./LongTextPreviewBlocks.vue";
7
+ export { default as LongTextInlineParts } from "./LongTextInlineParts.vue";
8
+ export { useScrollToBottom } from "./useScrollToBottom.js";
9
+ export { createAssistantTextSubmission } from "./submitText.js";
@@ -0,0 +1,29 @@
1
+ /** A companion's text enters the same draft and send action as keyboard input. */
2
+ export function createAssistantTextSubmission({ getState, setDraft, submit, afterDraftChange = async () => {} }) {
3
+ let revision = 0;
4
+ return async function submitText(text, { sendImmediately = true, signal, timeoutMs = 2000 } = {}) {
5
+ const request = ++revision;
6
+ const value = String(text || "").trim();
7
+ const initial = getState();
8
+ if (!value || !initial.active || signal?.aborted) throw new Error("The chat composer is not ready yet.");
9
+ const hasDraft = Boolean(initial.draft?.trim());
10
+ const writtenValue = hasDraft ? `${initial.draft}\n${value}` : value;
11
+ setDraft(writtenValue);
12
+ if (!sendImmediately || hasDraft) return "draft";
13
+ await afterDraftChange();
14
+ const deadline = Date.now() + timeoutMs;
15
+ while (true) {
16
+ const state = getState();
17
+ if (signal?.aborted || request !== revision || !state.active || state.id !== initial.id || state.draft !== writtenValue) {
18
+ throw new Error("The chat changed before sending. Review the draft before sending.");
19
+ }
20
+ if (state.canSend) {
21
+ const result = await submit();
22
+ if (result === false) throw new Error("Your words are in the chat box, but this conversation cannot send yet.");
23
+ return state.turnActive ? "steer" : "send";
24
+ }
25
+ if (Date.now() >= deadline) throw new Error("Your words are in the chat box, but this conversation cannot send yet.");
26
+ await new Promise((resolve) => setTimeout(resolve, 16));
27
+ }
28
+ };
29
+ }
@@ -0,0 +1,151 @@
1
+ import { nextTick, onBeforeUnmount, unref } from "vue";
2
+
3
+ const DEFAULT_SETTLE_DELAYS_MS = [0, 80];
4
+
5
+ function readElement(value) {
6
+ return unref(value) || null;
7
+ }
8
+
9
+ function hasWindowTimer(name) {
10
+ return typeof window !== "undefined" && typeof window[name] === "function";
11
+ }
12
+
13
+ function waitForLayoutFrame() {
14
+ if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
15
+ return Promise.resolve();
16
+ }
17
+
18
+ return new Promise((resolve) => {
19
+ window.requestAnimationFrame(resolve);
20
+ });
21
+ }
22
+
23
+ function normalizedScrollBehavior(value = "") {
24
+ return value === "smooth" ? "smooth" : "auto";
25
+ }
26
+
27
+ function scrollElementToBottom(targetElement, behavior = "auto") {
28
+ if (behavior === "smooth" && typeof targetElement.scrollTo === "function") {
29
+ targetElement.scrollTo({
30
+ behavior,
31
+ top: targetElement.scrollHeight
32
+ });
33
+ return;
34
+ }
35
+ targetElement.scrollTop = targetElement.scrollHeight;
36
+ }
37
+
38
+ function useScrollToBottom({
39
+ anchor = null,
40
+ enabled = true,
41
+ scrollAnchorIntoView = true,
42
+ settleDelaysMs = DEFAULT_SETTLE_DELAYS_MS,
43
+ target
44
+ } = {}) {
45
+ const scheduledTimers = new Set();
46
+ let disposed = false;
47
+ let scheduleVersion = 0;
48
+
49
+ function isEnabled() {
50
+ return unref(enabled) !== false;
51
+ }
52
+
53
+ function clearScheduledTimers() {
54
+ if (hasWindowTimer("clearTimeout")) {
55
+ scheduledTimers.forEach((timer) => {
56
+ window.clearTimeout(timer);
57
+ });
58
+ }
59
+ scheduledTimers.clear();
60
+ }
61
+
62
+ function clearScheduledScrolls() {
63
+ scheduleVersion += 1;
64
+ clearScheduledTimers();
65
+ }
66
+
67
+ function scrollNow(options = {}) {
68
+ if (disposed || !isEnabled()) {
69
+ return;
70
+ }
71
+
72
+ const targetElement = readElement(target);
73
+ if (!targetElement) {
74
+ return;
75
+ }
76
+
77
+ const behavior = normalizedScrollBehavior(options?.behavior);
78
+ scrollElementToBottom(targetElement, behavior);
79
+ if (scrollAnchorIntoView !== false) {
80
+ const anchorOptions = {
81
+ block: "end"
82
+ };
83
+ if (behavior === "smooth") {
84
+ anchorOptions.behavior = behavior;
85
+ }
86
+ readElement(anchor)?.scrollIntoView?.(anchorOptions);
87
+ }
88
+ scrollElementToBottom(targetElement, behavior);
89
+ }
90
+
91
+ function scheduleScroll(delayMs, options = {}, version = scheduleVersion) {
92
+ if (
93
+ disposed ||
94
+ version !== scheduleVersion ||
95
+ !isEnabled() ||
96
+ !hasWindowTimer("setTimeout")
97
+ ) {
98
+ return;
99
+ }
100
+
101
+ const timer = window.setTimeout(() => {
102
+ scheduledTimers.delete(timer);
103
+ if (version !== scheduleVersion) {
104
+ return;
105
+ }
106
+ scrollNow(options);
107
+ }, delayMs);
108
+ scheduledTimers.add(timer);
109
+ }
110
+
111
+ async function scrollAfterLayout(options = {}) {
112
+ if (disposed || !isEnabled()) {
113
+ return;
114
+ }
115
+ const version = scheduleVersion + 1;
116
+ scheduleVersion = version;
117
+ clearScheduledTimers();
118
+
119
+ await nextTick();
120
+ if (version !== scheduleVersion) {
121
+ return;
122
+ }
123
+ scrollNow(options);
124
+
125
+ await waitForLayoutFrame();
126
+ if (version !== scheduleVersion) {
127
+ return;
128
+ }
129
+ scrollNow(options);
130
+
131
+ clearScheduledTimers();
132
+ settleDelaysMs.forEach((delayMs) => {
133
+ scheduleScroll(delayMs, options, version);
134
+ });
135
+ }
136
+
137
+ onBeforeUnmount(() => {
138
+ disposed = true;
139
+ clearScheduledScrolls();
140
+ });
141
+
142
+ return {
143
+ clearScheduledScrolls,
144
+ scrollAfterLayout,
145
+ scrollNow
146
+ };
147
+ }
148
+
149
+ export {
150
+ useScrollToBottom
151
+ };