@jskit-ai/assistant-core 0.2.7 → 0.2.9
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/examples/conversation/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +13 -10
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
scrollDistanceFromBottom,
|
|
4
|
+
scrollElementNearBottom
|
|
5
|
+
} from "../../shared/conversation/scrollFollowState.js";
|
|
6
|
+
import { hasWindowFunction } from "../browser.js";
|
|
7
|
+
import { useScrollToBottom } from "./useScrollToBottom.js";
|
|
8
|
+
|
|
9
|
+
const USER_SCROLL_INTENT_RESET_MS = 600;
|
|
10
|
+
const LOAD_MORE_THRESHOLD_PX = 160;
|
|
11
|
+
const KEYBOARD_SCROLL_KEYS = new Set([
|
|
12
|
+
" ",
|
|
13
|
+
"ArrowDown",
|
|
14
|
+
"ArrowUp",
|
|
15
|
+
"End",
|
|
16
|
+
"Home",
|
|
17
|
+
"PageDown",
|
|
18
|
+
"PageUp",
|
|
19
|
+
"Spacebar"
|
|
20
|
+
]);
|
|
21
|
+
const UPWARD_SCROLL_KEYS = new Set(["ArrowUp", "Home", "PageUp"]);
|
|
22
|
+
|
|
23
|
+
function messageScrollKey(message = null) {
|
|
24
|
+
if (!message) {
|
|
25
|
+
return "empty";
|
|
26
|
+
}
|
|
27
|
+
return [
|
|
28
|
+
message.messageId || "",
|
|
29
|
+
message.role || "",
|
|
30
|
+
message.at || "",
|
|
31
|
+
String(message.text || "")
|
|
32
|
+
].join("/");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How the transcript follows its latest message: it settles at the end when it opens, keeps following new output
|
|
37
|
+
* while the reader stays at the end, stops as soon as the reader scrolls up, asks for older history near the top and
|
|
38
|
+
* keeps the reader's place when that history arrives or a retained transcript is shown again.
|
|
39
|
+
* @param {{
|
|
40
|
+
* props: {
|
|
41
|
+
* error: string,
|
|
42
|
+
* followLatestKey: number | string,
|
|
43
|
+
* hasMoreBefore: boolean,
|
|
44
|
+
* loading: boolean,
|
|
45
|
+
* loadingMore: boolean,
|
|
46
|
+
* loadMoreError: string,
|
|
47
|
+
* retainWhenHidden: boolean,
|
|
48
|
+
* scrollKey: number | string,
|
|
49
|
+
* visible: boolean
|
|
50
|
+
* },
|
|
51
|
+
* displayEntries: import("vue").ComputedRef<any[]>,
|
|
52
|
+
* isWorking: import("vue").ComputedRef<boolean>,
|
|
53
|
+
* bodyElement: import("vue").Ref<HTMLElement | null>,
|
|
54
|
+
* bottomElement: import("vue").Ref<HTMLElement | null>,
|
|
55
|
+
* loadMore: (request: { complete: (result?: { changed?: boolean }) => void }) => void
|
|
56
|
+
* }} options The transcript's props, its entries, its scrolled body and end marker, and how it asks for older history.
|
|
57
|
+
*/
|
|
58
|
+
function useTranscriptScrollFollow({ props, displayEntries, isWorking, bodyElement, bottomElement, loadMore }) {
|
|
59
|
+
const followingLatest = ref(true);
|
|
60
|
+
const initialScrollSettled = ref(false);
|
|
61
|
+
const userScrollIntent = ref(false);
|
|
62
|
+
let liveScrollFrame = 0;
|
|
63
|
+
let userScrollIntentTimer = null;
|
|
64
|
+
let initialScrollVersion = 0;
|
|
65
|
+
let loadMoreScrollSnapshot = null;
|
|
66
|
+
let retainedReaderScrollSnapshot = null;
|
|
67
|
+
let loadMoreRequestVersion = 0;
|
|
68
|
+
let pendingTailFollow = false;
|
|
69
|
+
let previousScrollTop = 0;
|
|
70
|
+
let previousScrollHeight = 0;
|
|
71
|
+
let userScrollIntentUpward = false;
|
|
72
|
+
let lastTouchY = null;
|
|
73
|
+
|
|
74
|
+
const loadingIndicatorVisible = computed(() => Boolean(
|
|
75
|
+
props.loading &&
|
|
76
|
+
!displayEntries.value.length
|
|
77
|
+
));
|
|
78
|
+
const initialScrollPending = computed(() => Boolean(
|
|
79
|
+
props.visible &&
|
|
80
|
+
displayEntries.value.length &&
|
|
81
|
+
!initialScrollSettled.value
|
|
82
|
+
));
|
|
83
|
+
|
|
84
|
+
const timelineScrollTrigger = computed(() => [
|
|
85
|
+
loadingIndicatorVisible.value ? "loading" : "ready",
|
|
86
|
+
displayEntries.value.length ? "has-messages" : "empty",
|
|
87
|
+
props.scrollKey
|
|
88
|
+
].join(":"));
|
|
89
|
+
const latestRenderedTailKey = computed(() => {
|
|
90
|
+
const entry = displayEntries.value.at(-1);
|
|
91
|
+
if (!entry) return "";
|
|
92
|
+
return [
|
|
93
|
+
entry.key,
|
|
94
|
+
isWorking.value,
|
|
95
|
+
entry.role === "thinking" ? entry.messages.map(messageScrollKey).join("+") : messageScrollKey(entry.message),
|
|
96
|
+
entry.turn.optimistic?.status || "",
|
|
97
|
+
entry.turn.optimistic?.error || ""
|
|
98
|
+
].join(":");
|
|
99
|
+
});
|
|
100
|
+
const autoScrollEnabled = computed(() => Boolean(
|
|
101
|
+
props.visible &&
|
|
102
|
+
followingLatest.value
|
|
103
|
+
));
|
|
104
|
+
|
|
105
|
+
const {
|
|
106
|
+
clearScheduledScrolls,
|
|
107
|
+
scrollAfterLayout: scrollToLatestMessage,
|
|
108
|
+
scrollNow: scrollToLatestMessageNow
|
|
109
|
+
} = useScrollToBottom({
|
|
110
|
+
anchor: bottomElement,
|
|
111
|
+
enabled: autoScrollEnabled,
|
|
112
|
+
scrollAnchorIntoView: false,
|
|
113
|
+
target: bodyElement
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function clearUserScrollIntent() {
|
|
117
|
+
if (userScrollIntentTimer && hasWindowFunction("clearTimeout")) {
|
|
118
|
+
window.clearTimeout(userScrollIntentTimer);
|
|
119
|
+
}
|
|
120
|
+
userScrollIntentTimer = null;
|
|
121
|
+
userScrollIntent.value = false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resumePendingTailFollow() {
|
|
125
|
+
const shouldResume = pendingTailFollow && followingLatest.value;
|
|
126
|
+
pendingTailFollow = false;
|
|
127
|
+
if (shouldResume) {
|
|
128
|
+
queueLiveBottomScroll();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function markUserScrollIntent({ upward = false } = {}) {
|
|
133
|
+
retainedReaderScrollSnapshot = null;
|
|
134
|
+
userScrollIntent.value = true;
|
|
135
|
+
userScrollIntentUpward = upward;
|
|
136
|
+
clearLiveBottomScroll();
|
|
137
|
+
clearScheduledScrolls();
|
|
138
|
+
if (userScrollIntentTimer && hasWindowFunction("clearTimeout")) {
|
|
139
|
+
window.clearTimeout(userScrollIntentTimer);
|
|
140
|
+
}
|
|
141
|
+
if (!hasWindowFunction("setTimeout")) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
userScrollIntentTimer = window.setTimeout(() => {
|
|
145
|
+
userScrollIntentTimer = null;
|
|
146
|
+
userScrollIntent.value = false;
|
|
147
|
+
resumePendingTailFollow();
|
|
148
|
+
}, USER_SCROLL_INTENT_RESET_MS);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function markKeyboardScrollIntent(event = {}) {
|
|
152
|
+
if (!KEYBOARD_SCROLL_KEYS.has(String(event.key || "")) || event.defaultPrevented) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const target = event.target;
|
|
156
|
+
const tagName = String(target?.tagName || "").toLowerCase();
|
|
157
|
+
if (
|
|
158
|
+
target?.isContentEditable ||
|
|
159
|
+
["input", "select", "textarea"].includes(tagName) ||
|
|
160
|
+
([" ", "Spacebar"].includes(event.key) && ["a", "button"].includes(tagName))
|
|
161
|
+
) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
markUserScrollIntent({
|
|
165
|
+
upward: UPWARD_SCROLL_KEYS.has(event.key) || (event.shiftKey && [" ", "Spacebar"].includes(event.key))
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function rememberTouch(event = {}) {
|
|
170
|
+
lastTouchY = event.touches?.[0]?.clientY ?? null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function markTouchScrollIntent(event = {}) {
|
|
174
|
+
const touchY = event.touches?.[0]?.clientY ?? null;
|
|
175
|
+
// A finger moving down the screen scrolls the transcript up.
|
|
176
|
+
markUserScrollIntent({ upward: touchY !== null && lastTouchY !== null && touchY > lastTouchY });
|
|
177
|
+
lastTouchY = touchY;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function scrollToLatestMessageAfterLayout({
|
|
181
|
+
behavior = "auto",
|
|
182
|
+
force = false
|
|
183
|
+
} = {}) {
|
|
184
|
+
if (force) {
|
|
185
|
+
retainedReaderScrollSnapshot = null;
|
|
186
|
+
followingLatest.value = true;
|
|
187
|
+
clearUserScrollIntent();
|
|
188
|
+
}
|
|
189
|
+
return scrollToLatestMessage({
|
|
190
|
+
behavior
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function queueInitialBottomScroll() {
|
|
195
|
+
const version = initialScrollVersion + 1;
|
|
196
|
+
initialScrollVersion = version;
|
|
197
|
+
initialScrollSettled.value = false;
|
|
198
|
+
if (props.retainWhenHidden && !props.visible) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
void scrollToLatestMessageAfterLayout({
|
|
202
|
+
behavior: "auto",
|
|
203
|
+
force: true
|
|
204
|
+
}).finally(() => {
|
|
205
|
+
if (initialScrollVersion === version) {
|
|
206
|
+
initialScrollSettled.value = true;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function queueLiveBottomScroll({
|
|
212
|
+
force = false
|
|
213
|
+
} = {}) {
|
|
214
|
+
if (userScrollIntent.value && !force) {
|
|
215
|
+
pendingTailFollow = true;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (force) {
|
|
219
|
+
retainedReaderScrollSnapshot = null;
|
|
220
|
+
pendingTailFollow = false;
|
|
221
|
+
followingLatest.value = true;
|
|
222
|
+
clearUserScrollIntent();
|
|
223
|
+
}
|
|
224
|
+
if (props.retainWhenHidden && !props.visible) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (liveScrollFrame) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!hasWindowFunction("requestAnimationFrame")) {
|
|
231
|
+
void scrollToLatestMessageAfterLayout({ behavior: "auto" });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
liveScrollFrame = window.requestAnimationFrame(() => {
|
|
235
|
+
liveScrollFrame = 0;
|
|
236
|
+
scrollToLatestMessageNow({ behavior: "auto" });
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function clearLiveBottomScroll() {
|
|
241
|
+
if (liveScrollFrame && hasWindowFunction("cancelAnimationFrame")) {
|
|
242
|
+
window.cancelAnimationFrame(liveScrollFrame);
|
|
243
|
+
}
|
|
244
|
+
liveScrollFrame = 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function updateLatestFollowFromScroll(event) {
|
|
248
|
+
if (props.retainWhenHidden && !props.visible) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const target = event?.currentTarget || bodyElement.value;
|
|
252
|
+
if (retainedReaderScrollSnapshot?.restoring) {
|
|
253
|
+
previousScrollTop = target?.scrollTop || 0;
|
|
254
|
+
previousScrollHeight = target?.scrollHeight || 0;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const scrollTop = target?.scrollTop || 0;
|
|
258
|
+
const scrollHeight = target?.scrollHeight || 0;
|
|
259
|
+
const scrolledUp = scrollTop < previousScrollTop;
|
|
260
|
+
// Layout changes such as scroll anchoring move scrollTop together with the
|
|
261
|
+
// content height; moving up over unchanged content is the reader's own.
|
|
262
|
+
const readerMovedUp = scrolledUp && scrollHeight === previousScrollHeight;
|
|
263
|
+
previousScrollTop = scrollTop;
|
|
264
|
+
previousScrollHeight = scrollHeight;
|
|
265
|
+
// Keep following the reader while the request is pending, before rows prepend.
|
|
266
|
+
if (loadMoreScrollSnapshot && !loadMoreScrollSnapshot.completed) {
|
|
267
|
+
loadMoreScrollSnapshot.scrollTop = scrollTop;
|
|
268
|
+
loadMoreScrollSnapshot.anchor = visibleHistoryAnchor(target);
|
|
269
|
+
loadMoreScrollSnapshot.scrollHeight = target.scrollHeight;
|
|
270
|
+
}
|
|
271
|
+
// The reader's own upward scrolling leaves the latest message from its first
|
|
272
|
+
// frame, even within the follow threshold.
|
|
273
|
+
const userScrollsUp = userScrollIntent.value && userScrollIntentUpward;
|
|
274
|
+
const shouldFollow = scrollElementNearBottom(target) &&
|
|
275
|
+
!(userScrollsUp && scrollDistanceFromBottom(target) > 1);
|
|
276
|
+
if (!shouldFollow && !userScrollIntent.value && !readerMovedUp && followingLatest.value) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
followingLatest.value = shouldFollow;
|
|
280
|
+
if (shouldFollow) {
|
|
281
|
+
// A scroll event still at the end, queued before upward input, must not
|
|
282
|
+
// end that input's intent.
|
|
283
|
+
if (!userScrollsUp) {
|
|
284
|
+
clearUserScrollIntent();
|
|
285
|
+
}
|
|
286
|
+
resumePendingTailFollow();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
clearLiveBottomScroll();
|
|
290
|
+
clearScheduledScrolls();
|
|
291
|
+
if (
|
|
292
|
+
scrolledUp &&
|
|
293
|
+
initialScrollSettled.value &&
|
|
294
|
+
scrollTop <= LOAD_MORE_THRESHOLD_PX &&
|
|
295
|
+
!props.loadMoreError
|
|
296
|
+
) {
|
|
297
|
+
requestLoadMore();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function visibleHistoryAnchor(element) {
|
|
302
|
+
const top = element.getBoundingClientRect().top;
|
|
303
|
+
for (const turn of element.querySelectorAll(".assistant-transcript__turn")) {
|
|
304
|
+
const bounds = turn.getBoundingClientRect();
|
|
305
|
+
if (bounds.bottom > top) {
|
|
306
|
+
return { element: turn, offset: bounds.top - top };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Offscreen turns use estimated heights. Preserve the visible anchor row instead
|
|
313
|
+
// of moving by the total height difference, which can change during layout.
|
|
314
|
+
function restoreAnchoredScroll(element, snapshot) {
|
|
315
|
+
const anchor = snapshot.anchor;
|
|
316
|
+
if (anchor && element.contains(anchor.element)) {
|
|
317
|
+
// Materialize the anchor row before reading the browser's adjusted scrollTop.
|
|
318
|
+
const offset = anchor.element.getBoundingClientRect().top - element.getBoundingClientRect().top;
|
|
319
|
+
previousScrollTop = element.scrollTop + offset - anchor.offset;
|
|
320
|
+
} else {
|
|
321
|
+
previousScrollTop = snapshot.scrollTop + (element.scrollHeight - snapshot.scrollHeight);
|
|
322
|
+
}
|
|
323
|
+
element.scrollTop = previousScrollTop;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Reopening materializes previously hidden content-visibility estimates.
|
|
327
|
+
async function waitForTwoFrames() {
|
|
328
|
+
if (hasWindowFunction("requestAnimationFrame")) {
|
|
329
|
+
await new Promise(resolve => window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function restoreRetainedReaderScroll() {
|
|
334
|
+
const snapshot = retainedReaderScrollSnapshot;
|
|
335
|
+
snapshot.restoring = true;
|
|
336
|
+
await nextTick();
|
|
337
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
338
|
+
const element = bodyElement.value;
|
|
339
|
+
if (
|
|
340
|
+
retainedReaderScrollSnapshot !== snapshot ||
|
|
341
|
+
element !== snapshot.body ||
|
|
342
|
+
snapshot.scrollKey !== props.scrollKey ||
|
|
343
|
+
!props.retainWhenHidden ||
|
|
344
|
+
!props.visible ||
|
|
345
|
+
followingLatest.value ||
|
|
346
|
+
loadMoreScrollSnapshot
|
|
347
|
+
) {
|
|
348
|
+
if (retainedReaderScrollSnapshot === snapshot) retainedReaderScrollSnapshot = null;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
restoreAnchoredScroll(element, snapshot);
|
|
352
|
+
if (pass === 0) await waitForTwoFrames();
|
|
353
|
+
}
|
|
354
|
+
if (retainedReaderScrollSnapshot === snapshot) retainedReaderScrollSnapshot = null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function requestLoadMore() {
|
|
358
|
+
if ((props.retainWhenHidden && !props.visible) || !props.hasMoreBefore || props.loadingMore || loadMoreScrollSnapshot) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
followingLatest.value = false;
|
|
362
|
+
pendingTailFollow = false;
|
|
363
|
+
markUserScrollIntent();
|
|
364
|
+
const element = bodyElement.value;
|
|
365
|
+
const version = loadMoreRequestVersion + 1;
|
|
366
|
+
loadMoreRequestVersion = version;
|
|
367
|
+
loadMoreScrollSnapshot = element
|
|
368
|
+
? {
|
|
369
|
+
anchor: visibleHistoryAnchor(element),
|
|
370
|
+
scrollHeight: element.scrollHeight,
|
|
371
|
+
scrollKey: props.scrollKey,
|
|
372
|
+
scrollTop: element.scrollTop,
|
|
373
|
+
version
|
|
374
|
+
}
|
|
375
|
+
: null;
|
|
376
|
+
loadMore({
|
|
377
|
+
complete: ({ changed = false } = {}) => {
|
|
378
|
+
void completeLoadMoreRequest(version, changed);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function clearLoadMoreScrollSnapshot() {
|
|
384
|
+
loadMoreRequestVersion += 1;
|
|
385
|
+
loadMoreScrollSnapshot = null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function completeLoadMoreRequest(version, changed) {
|
|
389
|
+
const snapshot = loadMoreScrollSnapshot;
|
|
390
|
+
if (!snapshot || snapshot.version !== version) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (!changed || snapshot.scrollKey !== props.scrollKey) {
|
|
394
|
+
clearLoadMoreScrollSnapshot();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
snapshot.completed = true;
|
|
398
|
+
await nextTick();
|
|
399
|
+
if (loadMoreScrollSnapshot?.version !== version) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const passes = props.retainWhenHidden ? 2 : 1;
|
|
403
|
+
for (let pass = 0; pass < passes; pass += 1) {
|
|
404
|
+
if (
|
|
405
|
+
(props.retainWhenHidden && !props.visible) ||
|
|
406
|
+
loadMoreScrollSnapshot?.version !== version ||
|
|
407
|
+
snapshot.scrollKey !== props.scrollKey
|
|
408
|
+
) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const element = bodyElement.value;
|
|
412
|
+
if (element) restoreAnchoredScroll(element, snapshot);
|
|
413
|
+
if (pass + 1 < passes) await waitForTwoFrames();
|
|
414
|
+
}
|
|
415
|
+
clearLoadMoreScrollSnapshot();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
onBeforeUnmount(() => {
|
|
419
|
+
retainedReaderScrollSnapshot = null;
|
|
420
|
+
clearLiveBottomScroll();
|
|
421
|
+
clearUserScrollIntent();
|
|
422
|
+
clearLoadMoreScrollSnapshot();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
watch(() => [
|
|
426
|
+
timelineScrollTrigger.value,
|
|
427
|
+
latestRenderedTailKey.value
|
|
428
|
+
], ([timelineKey, value], [previousTimelineKey, previous] = []) => {
|
|
429
|
+
if (timelineKey !== previousTimelineKey) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!value || value === previous) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
queueLiveBottomScroll({
|
|
436
|
+
force: false
|
|
437
|
+
});
|
|
438
|
+
}, {
|
|
439
|
+
flush: "post"
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
watch(() => [
|
|
443
|
+
props.scrollKey,
|
|
444
|
+
props.followLatestKey
|
|
445
|
+
], ([scrollKey, value], [previousScrollKey, previous] = []) => {
|
|
446
|
+
if (scrollKey !== previousScrollKey || value === previous) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
queueLiveBottomScroll({
|
|
450
|
+
force: true
|
|
451
|
+
});
|
|
452
|
+
}, {
|
|
453
|
+
flush: "post"
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
watch(timelineScrollTrigger, () => {
|
|
457
|
+
retainedReaderScrollSnapshot = null;
|
|
458
|
+
clearLoadMoreScrollSnapshot();
|
|
459
|
+
queueInitialBottomScroll();
|
|
460
|
+
}, {
|
|
461
|
+
flush: "post",
|
|
462
|
+
immediate: true
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
watch(() => props.error, () => {
|
|
466
|
+
if (followingLatest.value) queueLiveBottomScroll();
|
|
467
|
+
}, { flush: "post" });
|
|
468
|
+
|
|
469
|
+
// Capture before this transcript or its parent's v-show removes its geometry.
|
|
470
|
+
watch(() => props.visible, (visible) => {
|
|
471
|
+
if (visible) return;
|
|
472
|
+
retainedReaderScrollSnapshot = null;
|
|
473
|
+
const element = bodyElement.value;
|
|
474
|
+
if (
|
|
475
|
+
props.retainWhenHidden &&
|
|
476
|
+
initialScrollSettled.value &&
|
|
477
|
+
!followingLatest.value &&
|
|
478
|
+
!loadMoreScrollSnapshot &&
|
|
479
|
+
element?.clientHeight > 0
|
|
480
|
+
) {
|
|
481
|
+
retainedReaderScrollSnapshot = {
|
|
482
|
+
anchor: visibleHistoryAnchor(element),
|
|
483
|
+
body: element,
|
|
484
|
+
scrollHeight: element.scrollHeight,
|
|
485
|
+
scrollKey: props.scrollKey,
|
|
486
|
+
scrollTop: element.scrollTop
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}, { flush: "sync" });
|
|
490
|
+
|
|
491
|
+
watch(() => props.visible, (visible) => {
|
|
492
|
+
if (props.retainWhenHidden) {
|
|
493
|
+
if (!visible) {
|
|
494
|
+
clearLiveBottomScroll();
|
|
495
|
+
clearScheduledScrolls();
|
|
496
|
+
clearUserScrollIntent();
|
|
497
|
+
initialScrollVersion += 1;
|
|
498
|
+
} else if (loadMoreScrollSnapshot?.completed) {
|
|
499
|
+
void completeLoadMoreRequest(loadMoreScrollSnapshot.version, true);
|
|
500
|
+
} else if (!initialScrollSettled.value) {
|
|
501
|
+
queueInitialBottomScroll();
|
|
502
|
+
} else if (retainedReaderScrollSnapshot) {
|
|
503
|
+
void restoreRetainedReaderScroll();
|
|
504
|
+
} else {
|
|
505
|
+
queueLiveBottomScroll();
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
clearLoadMoreScrollSnapshot();
|
|
510
|
+
if (visible) queueLiveBottomScroll({ force: true });
|
|
511
|
+
}, { flush: "post" });
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
initialScrollPending,
|
|
515
|
+
loadingIndicatorVisible,
|
|
516
|
+
markKeyboardScrollIntent,
|
|
517
|
+
markTouchScrollIntent,
|
|
518
|
+
markUserScrollIntent,
|
|
519
|
+
rememberTouch,
|
|
520
|
+
requestLoadMore,
|
|
521
|
+
updateLatestFollowFromScroll
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export { useTranscriptScrollFollow };
|