@jskit-ai/assistant-core 0.2.2 → 0.2.4
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/fixtures/responsive-assistant/ConversationFixture.vue +2 -2
- package/fixtures/responsive-assistant/DeliveryFixture.vue +15 -2
- package/locales/en.json +4 -2
- package/package.json +5 -5
- package/src/client/conversation/AssistantConversationElement.vue +8 -1
- package/src/client/conversation/AssistantGoalControl.vue +2 -2
- package/src/client/conversation/AssistantTranscript.vue +49 -12
- package/src/client/conversation/messageDelivery.js +23 -6
- package/src/client/lib/assistantApi.js +5 -0
- package/src/server/conversation/claudeProcess.js +105 -58
- package/src/server/conversation/claudeTurn.js +179 -59
- package/src/server/conversation/codexAuthPreflight.js +3 -0
- package/src/server/conversation/codexConfiguration.js +42 -4
- package/src/server/conversation/codexConversations.js +256 -114
- package/src/server/conversation/codexEvents.js +16 -3
- package/src/server/conversation/codexHelperThreadRecovery.js +17 -4
- package/src/server/conversation/codexHelperThreads.js +10 -1
- package/src/server/conversation/codexHomeStorage.js +69 -0
- package/src/server/conversation/codexProcess.js +7 -2
- package/src/server/conversation/codexProvider.js +57 -46
- package/src/server/conversation/codexProviderOwner.js +234 -29
- package/src/server/conversation/codexProviderSelection.js +13 -3
- package/src/server/conversation/codexRuntimeLifecycle.js +56 -4
- package/src/server/conversation/codexSessionCleanup.js +67 -20
- package/src/server/conversation/codexSessionReconciliation.js +18 -4
- package/src/server/conversation/codexTurn.js +9 -2
- package/src/server/conversation/continuity.js +13 -1
- package/src/server/conversation/openCodeClient.js +34 -0
- package/src/server/conversation/openCodeProcess.js +37 -5
- package/src/server/conversation/openCodeRuntime.js +225 -64
- package/src/server/conversation/providers/claudeDriver.js +3 -3
- package/src/server/conversation/providers/codexDriver.js +10 -4
- package/src/server/conversation/providers/opencodeDriver.js +4 -2
- package/src/server/conversation/runtime.js +72 -2
- package/src/server/hosts/codexConfiguration.js +4 -0
- package/test/assistantScroll.browser.test.js +169 -10
- package/test/claudeLifecycle.test.js +293 -0
- package/test/claudeProcess.test.js +195 -51
- package/test/claudeTurn.test.js +174 -3
- package/test/codexAccountChange.test.js +261 -0
- package/test/codexConversationsCheckpoint.test.js +3 -0
- package/test/codexEvents.test.js +23 -1
- package/test/codexHomeStorage.test.js +57 -0
- package/test/codexInteractiveArguments.test.js +9 -0
- package/test/codexLifecycleIsolation.test.js +441 -0
- package/test/codexModelCatalog.test.js +48 -2
- package/test/codexProcess.test.js +21 -0
- package/test/codexTemporaryProviderRelease.test.js +180 -0
- package/test/codexTurn.test.js +21 -1
- package/test/conversationNativeStorage.test.js +57 -0
- package/test/conversationRuntime.test.js +66 -0
- package/test/conversationSession.test.js +4 -8
- package/test/messageDelivery.browser.test.js +18 -2
- package/test/messageDelivery.test.js +34 -1
- package/test/openCodeClient.test.js +31 -0
- package/test/openCodeConnectionChange.test.js +166 -0
- package/test/openCodeProcess.test.js +36 -0
- package/test/openCodeRuntime.test.js +25 -78
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.2.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
13
|
-
"@jskit-ai/kernel": "0.2.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.2.4",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.2.1",
|
|
13
|
+
"@jskit-ai/kernel": "0.2.1",
|
|
14
14
|
"@mdi/js": "^7.4.47",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0",
|
|
17
|
-
"@jskit-ai/connectors-core": "0.2.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.2.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.2.1",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.2.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
@@ -111,12 +111,12 @@ const adapter = reactive({
|
|
|
111
111
|
discard(id) {
|
|
112
112
|
if (id !== "temporary" || !previewCanTake.value) return;
|
|
113
113
|
temporaryPresent.value = false;
|
|
114
|
-
if (previewDelivery.find(id)?.status === "failed") previewDelivery.
|
|
114
|
+
if (previewDelivery.find(id)?.status === "failed") previewDelivery.remove(id);
|
|
115
115
|
},
|
|
116
116
|
edit(id) {
|
|
117
117
|
if (id !== "temporary" || !previewCanTake.value) return;
|
|
118
118
|
temporaryEditing.value = true;
|
|
119
|
-
if (previewDelivery.find(id)?.status === "failed") previewDelivery.
|
|
119
|
+
if (previewDelivery.find(id)?.status === "failed") previewDelivery.remove(id);
|
|
120
120
|
},
|
|
121
121
|
update(id, text) {
|
|
122
122
|
if (id === "temporary" && previewCanTake.value) temporaryWords.value = text;
|
|
@@ -25,6 +25,16 @@ const delivery = createAssistantMessageDelivery({ deliver(payload) {
|
|
|
25
25
|
pendingRequest = Promise.withResolvers();
|
|
26
26
|
return pendingRequest.promise;
|
|
27
27
|
} });
|
|
28
|
+
// A stand-in for the application's server cancel: the recovery fixture answers
|
|
29
|
+
// from its buttons, the others agree at once.
|
|
30
|
+
let cancelAnswer;
|
|
31
|
+
function cancelMessage(id) {
|
|
32
|
+
if (!delivery.cancel(id)) return false;
|
|
33
|
+
cancelAnswer = Promise.withResolvers();
|
|
34
|
+
cancelAnswer.promise.then(agreed => agreed ? delivery.remove(id) : delivery.cancelFailed(id));
|
|
35
|
+
if (!recoveryFixture) cancelAnswer.resolve(true);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
28
38
|
function accept() {
|
|
29
39
|
const payload = requests.value.at(-1);
|
|
30
40
|
const at = new Date().toISOString();
|
|
@@ -56,8 +66,8 @@ const adapter = reactive({
|
|
|
56
66
|
conversation: { cancellableDelivery: recoveryFixture, turns, visible: true, scrollKey: "delivery", welcomeMessage: "Start a conversation." },
|
|
57
67
|
composer: { draft, canSend: true, submitOnEnter: true, queueWhileSending },
|
|
58
68
|
actions: {
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
setDraft(value) { draft.value = value; },
|
|
70
|
+
cancel: cancelMessage
|
|
61
71
|
}
|
|
62
72
|
});
|
|
63
73
|
</script>
|
|
@@ -66,6 +76,9 @@ const adapter = reactive({
|
|
|
66
76
|
<main class="fixture">
|
|
67
77
|
<nav>
|
|
68
78
|
<button v-if="acceptedFixture" @click="turns.unshift({ turnId: 'earlier', user: { messageId: 'accepted-old', text: 'Earlier accepted message', at: '2026-10-16T11:05:00.000Z' } })">Load accepted history</button>
|
|
79
|
+
<button v-if="recoveryFixture" @click="delivery.find('uncertain').checking = true">Start delivery check</button>
|
|
80
|
+
<button v-if="recoveryFixture" @click="cancelAnswer?.resolve(false)">Server unreachable</button>
|
|
81
|
+
<button v-if="recoveryFixture" @click="cancelAnswer?.resolve(true)">Server agrees to cancel</button>
|
|
69
82
|
<button @click="pendingRequest.reject(new Error('Delivery unavailable.'))">Fail request</button>
|
|
70
83
|
<button @click="accept">Accept request</button>
|
|
71
84
|
<button @click="attachments.attachments.push({ attachmentId: 'new-file', fileName: 'next.txt' }); configuration = { tone: 'detailed' }">Prepare newer input</button>
|
package/locales/en.json
CHANGED
|
@@ -87,6 +87,8 @@
|
|
|
87
87
|
"useSuggestion": "Use suggestion: {prompt}"
|
|
88
88
|
},
|
|
89
89
|
"delivery": {
|
|
90
|
+
"cancelling": "Cancelling…",
|
|
91
|
+
"cancelUnreachable": "Couldn't reach the server — Cancel again",
|
|
90
92
|
"checkBeforeResend": "Check delivery before resending.",
|
|
91
93
|
"checkPrevious": "Check the previous message's delivery before sending again.",
|
|
92
94
|
"checking": "Checking…",
|
|
@@ -213,6 +215,8 @@
|
|
|
213
215
|
"liveSettingsUnsupported": "This provider does not support live native settings.",
|
|
214
216
|
"completedEnvelopesNeedStorage": "Completed envelopes require runtime-owned canonical storage.",
|
|
215
217
|
"originalReadOnly": "This conversation uses its original read and subscription operations.",
|
|
218
|
+
"messageCancelUnsupported": "This conversation cannot cancel one message.",
|
|
219
|
+
"messageCancelled": "This message was cancelled. Send it again as a new message.",
|
|
216
220
|
"authorizedSelectionRequired": "Use the conversation's authorized selection operation.",
|
|
217
221
|
"eventDeliveryRetained": "This scoped conversation retains its original event delivery.",
|
|
218
222
|
"engineChanged": "The selected native engine changed.",
|
|
@@ -371,8 +375,6 @@
|
|
|
371
375
|
"conversation_receipt_unavailable": "The native receipt has no stored authored message.",
|
|
372
376
|
"conversation_goal_unavailable": "The native goal is unavailable. Try reading its status again.",
|
|
373
377
|
"assistant_provider_invalid_response": "Assistant provider returned an invalid response payload.",
|
|
374
|
-
"assistant_opencode_connection_changing": "OpenCode is changing a connection. Retry after the connection change finishes.",
|
|
375
|
-
"assistant_opencode_connection_in_use": "Close the other OpenCode conversations that use this connection, then retry this connection change. The saved connection has not changed.",
|
|
376
378
|
"assistant_opencode_conversation_busy": "This conversation is still working.",
|
|
377
379
|
"assistant_opencode_key_verification_failed": "OpenCode rejected the API-key verification request.",
|
|
378
380
|
"assistant_opencode_turn_thread_mismatch": "The OpenCode predecessor history changed before handover generation.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
49
49
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
50
50
|
"@ai-sdk/xai": "^4.0.58",
|
|
51
|
-
"@jskit-ai/resource-core": "0.1.
|
|
52
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
51
|
+
"@jskit-ai/resource-core": "0.1.192",
|
|
52
|
+
"@jskit-ai/resource-crud-core": "0.1.192",
|
|
53
53
|
"@mdi/js": "^7.4.47",
|
|
54
54
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
55
55
|
"ai": "^7.0.101",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"ws": "^8.20.1"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
62
|
-
"@jskit-ai/kernel": "0.2.
|
|
61
|
+
"@jskit-ai/http-runtime": "0.2.1",
|
|
62
|
+
"@jskit-ai/kernel": "0.2.1",
|
|
63
63
|
"vue": "^3.5.13",
|
|
64
64
|
"vuetify": "^4.0.0"
|
|
65
65
|
},
|
|
@@ -327,8 +327,15 @@ async function resend(id) {
|
|
|
327
327
|
}
|
|
328
328
|
catch { /* The delivery controller retains the error beside the failed message. */ }
|
|
329
329
|
}
|
|
330
|
+
// The application's cancel asks its server and removes the message only once
|
|
331
|
+
// the server agrees. Without one, only a message the server already refused
|
|
332
|
+
// (failed) is removed.
|
|
330
333
|
function cancel(id) {
|
|
331
|
-
|
|
334
|
+
if (props.adapter.actions?.cancel) return props.adapter.actions.cancel(id);
|
|
335
|
+
const message = props.adapter.delivery?.find(id);
|
|
336
|
+
if (message?.status !== "failed") return false;
|
|
337
|
+
props.adapter.delivery.remove(id);
|
|
338
|
+
return true;
|
|
332
339
|
}
|
|
333
340
|
function edit(id) {
|
|
334
341
|
if (props.adapter.actions?.edit) return props.adapter.actions.edit(id);
|
|
@@ -70,9 +70,9 @@ async function setGoal() {
|
|
|
70
70
|
</template>
|
|
71
71
|
<v-card class="assistant-goal__details pa-3" max-width="360">
|
|
72
72
|
<strong role="status">{{ label }}</strong>
|
|
73
|
-
<v-btn v-if="(running || (state.pauseBlocked && goal?.status === 'blocked')) && state.pause" size="small" :disabled="state.
|
|
73
|
+
<v-btn v-if="(running || (state.pauseBlocked && goal?.status === 'blocked')) && state.pause" size="small" :disabled="state.stopPending" @click="state.pause()">{{ state.action === 'pause' ? t('jskit.assistant.goal.pausing') : t('jskit.assistant.goal.pause') }}</v-btn>
|
|
74
74
|
<v-btn v-if="['paused', 'blocked', 'usageLimited'].includes(goal?.status) && state.resume" size="small" :disabled="state.pending" @click="state.resume()">{{ state.action === 'resume' ? t('jskit.assistant.goal.resuming') : t('jskit.assistant.goal.resume') }}</v-btn>
|
|
75
|
-
<v-btn v-if="goal && goal.status !== 'complete' && state.cancel" color="error" size="small" variant="text" :disabled="state.
|
|
75
|
+
<v-btn v-if="goal && goal.status !== 'complete' && state.cancel" color="error" size="small" variant="text" :disabled="state.stopPending" @click="state.cancel()">{{ state.action === 'cancel' ? t('jskit.assistant.goal.cancelling') : t('jskit.assistant.goal.cancel') }}</v-btn>
|
|
76
76
|
<p v-if="goal" class="my-2">{{ goal.objective }}</p>
|
|
77
77
|
<p v-if="goal?.reason" class="text-body-small my-2">{{ goal.reason }}</p>
|
|
78
78
|
<p v-if="elapsed" class="text-body-small my-2">{{ elapsedText }}</p>
|
|
@@ -65,10 +65,11 @@
|
|
|
65
65
|
:class="{ 'assistant-transcript__body--settling': initialScrollPending }"
|
|
66
66
|
tabindex="0"
|
|
67
67
|
@keydown.capture="markKeyboardScrollIntent"
|
|
68
|
-
@pointerdown.self="markUserScrollIntent"
|
|
68
|
+
@pointerdown.self="markUserScrollIntent()"
|
|
69
69
|
@scroll.passive="updateLatestFollowFromScroll"
|
|
70
|
-
@
|
|
71
|
-
@
|
|
70
|
+
@touchstart.passive="rememberTouch"
|
|
71
|
+
@touchmove.passive="markTouchScrollIntent"
|
|
72
|
+
@wheel.passive="markUserScrollIntent({ upward: $event.deltaY < 0 })"
|
|
72
73
|
>
|
|
73
74
|
<div
|
|
74
75
|
v-if="welcomeMessage"
|
|
@@ -179,12 +180,15 @@
|
|
|
179
180
|
</div>
|
|
180
181
|
<slot name="user-message-actions" :message="entry.message" :turn="entry.turn" />
|
|
181
182
|
<div
|
|
182
|
-
v-if="entry.delivery || entry.turn.optimistic?.status === 'failed'"
|
|
183
|
+
v-if="entry.delivery || entry.turn.optimistic?.status === 'failed' || entry.turn.optimistic?.cancelling || entry.turn.optimistic?.cancelError"
|
|
183
184
|
class="assistant-transcript__optimistic-failure"
|
|
184
185
|
>
|
|
185
|
-
<span role="status">{{ entry.
|
|
186
|
+
<span role="status">{{ entry.turn.optimistic?.cancelling ? t('jskit.assistant.delivery.cancelling')
|
|
187
|
+
: entry.turn.optimistic?.cancelError ? entry.turn.optimistic.cancelError
|
|
188
|
+
: entry.delivery ? entry.delivery.delivery.error || t('jskit.assistant.delivery.unconfirmedShort') : t('jskit.assistant.transcript.failed', { error: entry.turn.optimistic.error || t('jskit.assistant.delivery.notSent') }) }}</span>
|
|
186
189
|
<div v-if="!entry.turn.preview" class="assistant-transcript__optimistic-actions">
|
|
187
190
|
<v-btn
|
|
191
|
+
v-if="!entry.turn.optimistic?.cancelling && !entry.turn.optimistic?.cancelError && (entry.delivery || entry.turn.optimistic?.status === 'failed')"
|
|
188
192
|
class="assistant-transcript__delivery-action"
|
|
189
193
|
color="error"
|
|
190
194
|
size="x-small"
|
|
@@ -198,7 +202,7 @@
|
|
|
198
202
|
{{ entry.delivery?.delivery.checking ? t('jskit.assistant.delivery.checking') : t('jskit.assistant.delivery.retry') }}
|
|
199
203
|
</v-btn>
|
|
200
204
|
<v-btn
|
|
201
|
-
v-if="!entry.delivery || cancellableDelivery"
|
|
205
|
+
v-if="!entry.delivery || cancellableDelivery || entry.turn.optimistic?.cancelling || entry.turn.optimistic?.cancelError"
|
|
202
206
|
class="assistant-transcript__delivery-action"
|
|
203
207
|
size="x-small"
|
|
204
208
|
type="button"
|
|
@@ -208,7 +212,7 @@
|
|
|
208
212
|
{{ t("jskit.assistant.transcript.cancel") }}
|
|
209
213
|
</v-btn>
|
|
210
214
|
<v-btn
|
|
211
|
-
v-if="!entry.delivery"
|
|
215
|
+
v-if="!entry.delivery && entry.turn.optimistic?.status === 'failed' && !entry.turn.optimistic.cancelling && !entry.turn.optimistic.cancelError"
|
|
212
216
|
class="assistant-transcript__delivery-action"
|
|
213
217
|
size="x-small"
|
|
214
218
|
type="button"
|
|
@@ -328,6 +332,7 @@ import AssistantProgress from "./AssistantProgress.vue";
|
|
|
328
332
|
import { parseNumberedQuestionPrompt } from "../../shared/conversation/numberedQuestionSugar.js";
|
|
329
333
|
import { parseLongTextReviewBlocks } from "../../shared/conversation/longTextBlocks.js";
|
|
330
334
|
import {
|
|
335
|
+
scrollDistanceFromBottom,
|
|
331
336
|
scrollElementNearBottom
|
|
332
337
|
} from "../../shared/conversation/scrollFollowState.js";
|
|
333
338
|
import {
|
|
@@ -442,6 +447,9 @@ let retainedReaderScrollSnapshot = null;
|
|
|
442
447
|
let loadMoreRequestVersion = 0;
|
|
443
448
|
let pendingTailFollow = false;
|
|
444
449
|
let previousScrollTop = 0;
|
|
450
|
+
let previousScrollHeight = 0;
|
|
451
|
+
let userScrollIntentUpward = false;
|
|
452
|
+
let lastTouchY = null;
|
|
445
453
|
const USER_SCROLL_INTENT_RESET_MS = 600;
|
|
446
454
|
const LOAD_MORE_THRESHOLD_PX = 160;
|
|
447
455
|
const KEYBOARD_SCROLL_KEYS = new Set([
|
|
@@ -454,6 +462,7 @@ const KEYBOARD_SCROLL_KEYS = new Set([
|
|
|
454
462
|
"PageUp",
|
|
455
463
|
"Spacebar"
|
|
456
464
|
]);
|
|
465
|
+
const UPWARD_SCROLL_KEYS = new Set(["ArrowUp", "Home", "PageUp"]);
|
|
457
466
|
const TIME_FORMAT = Object.freeze({
|
|
458
467
|
year: "numeric",
|
|
459
468
|
month: "short",
|
|
@@ -747,9 +756,10 @@ function resumePendingTailFollow() {
|
|
|
747
756
|
}
|
|
748
757
|
}
|
|
749
758
|
|
|
750
|
-
function markUserScrollIntent() {
|
|
759
|
+
function markUserScrollIntent({ upward = false } = {}) {
|
|
751
760
|
retainedReaderScrollSnapshot = null;
|
|
752
761
|
userScrollIntent.value = true;
|
|
762
|
+
userScrollIntentUpward = upward;
|
|
753
763
|
clearLiveBottomScroll();
|
|
754
764
|
clearScheduledScrolls();
|
|
755
765
|
if (userScrollIntentTimer && typeof window !== "undefined" && typeof window.clearTimeout === "function") {
|
|
@@ -778,7 +788,20 @@ function markKeyboardScrollIntent(event = {}) {
|
|
|
778
788
|
) {
|
|
779
789
|
return;
|
|
780
790
|
}
|
|
781
|
-
markUserScrollIntent(
|
|
791
|
+
markUserScrollIntent({
|
|
792
|
+
upward: UPWARD_SCROLL_KEYS.has(event.key) || (event.shiftKey && [" ", "Spacebar"].includes(event.key))
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function rememberTouch(event = {}) {
|
|
797
|
+
lastTouchY = event.touches?.[0]?.clientY ?? null;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function markTouchScrollIntent(event = {}) {
|
|
801
|
+
const touchY = event.touches?.[0]?.clientY ?? null;
|
|
802
|
+
// A finger moving down the screen scrolls the transcript up.
|
|
803
|
+
markUserScrollIntent({ upward: touchY !== null && lastTouchY !== null && touchY > lastTouchY });
|
|
804
|
+
lastTouchY = touchY;
|
|
782
805
|
}
|
|
783
806
|
|
|
784
807
|
function scrollToLatestMessageAfterLayout({
|
|
@@ -859,24 +882,38 @@ function updateLatestFollowFromScroll(event = {}) {
|
|
|
859
882
|
const target = event?.currentTarget || bodyElement.value;
|
|
860
883
|
if (retainedReaderScrollSnapshot?.restoring) {
|
|
861
884
|
previousScrollTop = target?.scrollTop || 0;
|
|
885
|
+
previousScrollHeight = target?.scrollHeight || 0;
|
|
862
886
|
return;
|
|
863
887
|
}
|
|
864
888
|
const scrollTop = target?.scrollTop || 0;
|
|
889
|
+
const scrollHeight = target?.scrollHeight || 0;
|
|
865
890
|
const scrolledUp = scrollTop < previousScrollTop;
|
|
891
|
+
// Layout changes such as scroll anchoring move scrollTop together with the
|
|
892
|
+
// content height; moving up over unchanged content is the reader's own.
|
|
893
|
+
const readerMovedUp = scrolledUp && scrollHeight === previousScrollHeight;
|
|
866
894
|
previousScrollTop = scrollTop;
|
|
895
|
+
previousScrollHeight = scrollHeight;
|
|
867
896
|
// Keep following the reader while the request is pending, before rows prepend.
|
|
868
897
|
if (loadMoreScrollSnapshot && !loadMoreScrollSnapshot.completed) {
|
|
869
898
|
loadMoreScrollSnapshot.scrollTop = scrollTop;
|
|
870
899
|
loadMoreScrollSnapshot.anchor = visibleHistoryAnchor(target);
|
|
871
900
|
loadMoreScrollSnapshot.scrollHeight = target.scrollHeight;
|
|
872
901
|
}
|
|
873
|
-
|
|
874
|
-
|
|
902
|
+
// The reader's own upward scrolling leaves the latest message from its first
|
|
903
|
+
// frame, even within the follow threshold.
|
|
904
|
+
const userScrollsUp = userScrollIntent.value && userScrollIntentUpward;
|
|
905
|
+
const shouldFollow = scrollElementNearBottom(target) &&
|
|
906
|
+
!(userScrollsUp && scrollDistanceFromBottom(target) > 1);
|
|
907
|
+
if (!shouldFollow && !userScrollIntent.value && !readerMovedUp && followingLatest.value) {
|
|
875
908
|
return;
|
|
876
909
|
}
|
|
877
910
|
followingLatest.value = shouldFollow;
|
|
878
911
|
if (shouldFollow) {
|
|
879
|
-
|
|
912
|
+
// A scroll event still at the end, queued before upward input, must not
|
|
913
|
+
// end that input's intent.
|
|
914
|
+
if (!userScrollsUp) {
|
|
915
|
+
clearUserScrollIntent();
|
|
916
|
+
}
|
|
880
917
|
resumePendingTailFollow();
|
|
881
918
|
return;
|
|
882
919
|
}
|
|
@@ -100,7 +100,8 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
100
100
|
} : null;
|
|
101
101
|
return {
|
|
102
102
|
optimistic: { error: message.error, id: message.id, status: message.status,
|
|
103
|
-
...(message.status === "pending" && message.waiting ? { waiting: message.waiting } : {})
|
|
103
|
+
...(message.status === "pending" && message.waiting ? { waiting: message.waiting } : {}),
|
|
104
|
+
...(message.cancelling ? { cancelling: true } : {}), ...(message.cancelError ? { cancelError: message.cancelError } : {}) },
|
|
104
105
|
turnId: message.id,
|
|
105
106
|
[role]: authored,
|
|
106
107
|
...(status ? { system: role === "system" ? { ...authored, ...status, text: `${authored.text}\n\n${status.text}` } : status } : {})
|
|
@@ -117,7 +118,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
117
118
|
queue = false
|
|
118
119
|
} = {}) {
|
|
119
120
|
if ((state.sending && !queue) || pendingSends.has(messageId) || find(messageId)?.status === "uncertain" ||
|
|
120
|
-
(!messageText(payload?.message) && !payload?.displayAttachments?.length)) return false;
|
|
121
|
+
find(messageId)?.cancelling || (!messageText(payload?.message) && !payload?.displayAttachments?.length)) return false;
|
|
121
122
|
if (typeof deliver !== "function") throw new TypeError("Message delivery requires a deliver function.");
|
|
122
123
|
const snapshot = JSON.parse(JSON.stringify(payload));
|
|
123
124
|
const previous = find(messageId);
|
|
@@ -147,6 +148,8 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
147
148
|
try {
|
|
148
149
|
if (predecessor) await predecessor;
|
|
149
150
|
if (!current()) return false;
|
|
151
|
+
// A queued message cancelled while it waited is never delivered.
|
|
152
|
+
if (find(messageId)?.cancelling) return false;
|
|
150
153
|
if (uncertainOnError && state.messages.some(entry => entry.id !== messageId && entry.status === "uncertain" && !knownUncertain.has(entry.id))) {
|
|
151
154
|
fail(messageId, t("jskit.assistant.delivery.checkPrevious"));
|
|
152
155
|
return false;
|
|
@@ -240,15 +243,29 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
240
243
|
return true;
|
|
241
244
|
}
|
|
242
245
|
|
|
246
|
+
// Cancel of a pending, uncertain or failed message waits for the server: the
|
|
247
|
+
// message stays, shown as cancelling, until the caller removes it on the
|
|
248
|
+
// server's agreement or reports that the server could not be reached.
|
|
243
249
|
function cancel(messageId) {
|
|
244
|
-
|
|
245
|
-
|
|
250
|
+
const message = find(messageId);
|
|
251
|
+
if (!message || message.status === "accepted" || message.cancelling) return false;
|
|
252
|
+
message.cancelling = true;
|
|
253
|
+
message.cancelError = "";
|
|
246
254
|
return true;
|
|
247
255
|
}
|
|
248
256
|
|
|
257
|
+
function cancelFailed(messageId, error = "") {
|
|
258
|
+
const message = find(messageId);
|
|
259
|
+
if (!message) return;
|
|
260
|
+
message.cancelling = false;
|
|
261
|
+
message.cancelError = String(error || t("jskit.assistant.delivery.cancelUnreachable")).trim();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Only a message that never reached the conversation returns to the draft.
|
|
249
265
|
function edit(messageId, draft = "") {
|
|
250
266
|
const message = find(messageId);
|
|
251
|
-
if (
|
|
267
|
+
if (message?.status !== "failed" || message.cancelling) return null;
|
|
268
|
+
remove(messageId);
|
|
252
269
|
return draftWithRestoredMessage(message.text, draft);
|
|
253
270
|
}
|
|
254
271
|
|
|
@@ -269,7 +286,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
269
286
|
state.sending = false;
|
|
270
287
|
}
|
|
271
288
|
|
|
272
|
-
return { accept, cancel, edit, find, reconcile, remove, resend, reset, restoreUncertain, rejectUnsent, send, state, turns, wait };
|
|
289
|
+
return { accept, cancel, cancelFailed, edit, find, reconcile, remove, resend, reset, restoreUncertain, rejectUnsent, send, state, turns, wait };
|
|
273
290
|
}
|
|
274
291
|
|
|
275
292
|
// The composer text after a failed or cancelled message returns to it: the
|
|
@@ -133,6 +133,11 @@ function createAssistantApi({ request, requestStream, resolveBasePath, resolveSu
|
|
|
133
133
|
return requestConversation(conversationId, `/deliveries/${encodedMessageId}/inspect`, { method: "POST", signal });
|
|
134
134
|
},
|
|
135
135
|
|
|
136
|
+
cancelConversationDelivery(conversationId, messageId, { signal } = {}) {
|
|
137
|
+
const encodedMessageId = encodeURIComponent(String(messageId || "").trim());
|
|
138
|
+
return requestConversation(conversationId, `/deliveries/${encodedMessageId}/cancel`, { method: "POST", signal });
|
|
139
|
+
},
|
|
140
|
+
|
|
136
141
|
readConversationGoal(conversationId, { signal } = {}) {
|
|
137
142
|
return requestConversation(conversationId, "/goal", { method: "GET", signal });
|
|
138
143
|
},
|