@jskit-ai/assistant-core 0.2.7 → 0.2.8
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,158 @@
|
|
|
1
|
+
// Which files a drop, a paste or a file list carries, read from the browser's transfer objects, and the drop and
|
|
2
|
+
// paste target that hands them to an upload queue.
|
|
3
|
+
import { computed, ref } from "vue";
|
|
4
|
+
import { useJskitText } from "../i18n.js";
|
|
5
|
+
|
|
6
|
+
const { t } = useJskitText();
|
|
7
|
+
|
|
8
|
+
function assistantAttachmentFiles(fileList) {
|
|
9
|
+
return Array.from(fileList || []).filter((file) => file && file.size >= 0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function assistantAttachmentFilesFromTransferItems(items) {
|
|
13
|
+
return assistantAttachmentFiles(Array.from(items || [])
|
|
14
|
+
.map((item) => {
|
|
15
|
+
// readable-check: allow method-probe - platform object: a transfer item, or a caller's stand-in for one, may hold no file
|
|
16
|
+
if (item?.kind !== "file" || typeof item.getAsFile !== "function") {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return item.getAsFile();
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assistantAttachmentFilesFromDropEvent(event) {
|
|
24
|
+
const dataTransfer = event?.dataTransfer;
|
|
25
|
+
const itemFiles = assistantAttachmentFilesFromTransferItems(dataTransfer?.items);
|
|
26
|
+
if (itemFiles.length > 0) {
|
|
27
|
+
return itemFiles;
|
|
28
|
+
}
|
|
29
|
+
return assistantAttachmentFiles(dataTransfer?.files);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assistantAttachmentFilesFromPasteEvent(event) {
|
|
33
|
+
const clipboardData = event?.clipboardData;
|
|
34
|
+
const itemFiles = assistantAttachmentFilesFromTransferItems(clipboardData?.items);
|
|
35
|
+
if (itemFiles.length > 0) {
|
|
36
|
+
return itemFiles;
|
|
37
|
+
}
|
|
38
|
+
return assistantAttachmentFiles(clipboardData?.files);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function assistantAttachmentEventHasFiles(event) {
|
|
42
|
+
const dataTransfer = event?.dataTransfer;
|
|
43
|
+
return assistantAttachmentFilesFromDropEvent(event).length > 0 ||
|
|
44
|
+
Array.from(dataTransfer?.items || []).some((item) => item?.kind === "file") ||
|
|
45
|
+
Array.from(dataTransfer?.types || []).some((type) => (
|
|
46
|
+
type === "Files" ||
|
|
47
|
+
type === "application/x-moz-file"
|
|
48
|
+
));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assistantAttachmentClipboardText(event) {
|
|
52
|
+
// readable-check: allow method-probe - platform object: a paste event, or a caller's stand-in for one, may carry no clipboard text
|
|
53
|
+
if (typeof event?.clipboardData?.getData !== "function") {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
return String(event.clipboardData.getData("text/plain") || "");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assistantAttachmentClipboardLocalFileReference(event) {
|
|
60
|
+
const clipboardData = event?.clipboardData;
|
|
61
|
+
// readable-check: allow method-probe - platform object: a paste event, or a caller's stand-in for one, may carry no clipboard text
|
|
62
|
+
if (!clipboardData || typeof clipboardData.getData !== "function") {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
const types = Array.from(clipboardData.types || []);
|
|
66
|
+
const text = [
|
|
67
|
+
clipboardData.getData("x-special/gnome-copied-files"),
|
|
68
|
+
clipboardData.getData("text/uri-list"),
|
|
69
|
+
clipboardData.getData("text/plain")
|
|
70
|
+
].map((value) => String(value || "").trim()).filter(Boolean).join("\n");
|
|
71
|
+
if (
|
|
72
|
+
types.includes("Files") ||
|
|
73
|
+
types.includes("application/x-moz-file") ||
|
|
74
|
+
types.includes("x-special/gnome-copied-files") ||
|
|
75
|
+
/^file:/imu.test(text)
|
|
76
|
+
) {
|
|
77
|
+
return text || "file://";
|
|
78
|
+
}
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A drop zone and paste target that hands the files it receives to `uploadFiles`. Nested drag enters are counted, so
|
|
84
|
+
* the zone stays active until the drag leaves it.
|
|
85
|
+
* @param {{
|
|
86
|
+
* uploadFiles: (files: File[]) => Promise<any[]>,
|
|
87
|
+
* reportAttachmentError: (error: unknown) => string
|
|
88
|
+
* }} dependencies
|
|
89
|
+
*/
|
|
90
|
+
function useAttachmentDropTarget({ uploadFiles, reportAttachmentError }) {
|
|
91
|
+
const dragDepth = ref(0);
|
|
92
|
+
const dragActive = computed(() => dragDepth.value > 0);
|
|
93
|
+
|
|
94
|
+
function resetDragState() {
|
|
95
|
+
dragDepth.value = 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handleDragEnter(event) {
|
|
99
|
+
if (!assistantAttachmentEventHasFiles(event)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
dragDepth.value += 1;
|
|
103
|
+
if (event?.dataTransfer) {
|
|
104
|
+
event.dataTransfer.dropEffect = "copy";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleDragOver(event) {
|
|
109
|
+
if (event?.dataTransfer) {
|
|
110
|
+
event.dataTransfer.dropEffect = "copy";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function handleDragLeave() {
|
|
115
|
+
dragDepth.value = Math.max(0, dragDepth.value - 1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function handleDrop(event) {
|
|
119
|
+
resetDragState();
|
|
120
|
+
return uploadFiles(assistantAttachmentFilesFromDropEvent(event));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function handlePaste(event) {
|
|
124
|
+
const files = assistantAttachmentFilesFromPasteEvent(event);
|
|
125
|
+
if (files.length < 1) {
|
|
126
|
+
if (assistantAttachmentClipboardLocalFileReference(event)) {
|
|
127
|
+
reportAttachmentError(t("jskit.assistant.attachments.error.localFilePaste"));
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
if (!assistantAttachmentClipboardText(event)) {
|
|
132
|
+
// readable-check: allow optional-call - platform object: a paste event or a caller's stand-in for one
|
|
133
|
+
event?.preventDefault?.();
|
|
134
|
+
}
|
|
135
|
+
return uploadFiles(files);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
dragActive,
|
|
140
|
+
handleDragEnter,
|
|
141
|
+
handleDragLeave,
|
|
142
|
+
handleDragOver,
|
|
143
|
+
handleDrop,
|
|
144
|
+
handlePaste,
|
|
145
|
+
resetDragState
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
assistantAttachmentClipboardLocalFileReference,
|
|
151
|
+
assistantAttachmentClipboardText,
|
|
152
|
+
assistantAttachmentEventHasFiles,
|
|
153
|
+
assistantAttachmentFiles,
|
|
154
|
+
assistantAttachmentFilesFromDropEvent,
|
|
155
|
+
assistantAttachmentFilesFromPasteEvent,
|
|
156
|
+
assistantAttachmentFilesFromTransferItems,
|
|
157
|
+
useAttachmentDropTarget
|
|
158
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// How the attachment queue shows each item: its key, name, phase, progress, status text, icon and colour.
|
|
2
|
+
import { mdiAlertCircleOutline, mdiCheckCircleOutline, mdiFileOutline } from "@mdi/js";
|
|
3
|
+
import { useJskitText } from "../i18n.js";
|
|
4
|
+
import { attachmentSizeText } from "./attachmentSize.js";
|
|
5
|
+
|
|
6
|
+
const { t } = useJskitText();
|
|
7
|
+
|
|
8
|
+
const activePhases = new Set(["queued", "preparing", "uploading", "delivering"]);
|
|
9
|
+
|
|
10
|
+
function itemKey(item = {}) {
|
|
11
|
+
return String(item.clientId || item.id || item.attachmentId || item.fileName || "attachment");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function itemName(item = {}) {
|
|
15
|
+
return String(item.fileName || item.file?.name || t("jskit.assistant.attachments.unnamed"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function itemPhase(item = {}) {
|
|
19
|
+
return String(item.phase || item.status || "queued");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function itemPercent(item = {}) {
|
|
23
|
+
if (itemPhase(item) === "ready") {
|
|
24
|
+
return 100;
|
|
25
|
+
}
|
|
26
|
+
if (itemPhase(item) === "delivering") {
|
|
27
|
+
return 100;
|
|
28
|
+
}
|
|
29
|
+
if (itemPhase(item) === "queued") {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
const progressValue = item.percent ?? item.progress;
|
|
33
|
+
const directPercent = progressValue === null || progressValue === undefined || progressValue === ""
|
|
34
|
+
? null
|
|
35
|
+
: Number(progressValue);
|
|
36
|
+
if (directPercent !== null && Number.isFinite(directPercent)) {
|
|
37
|
+
return Math.min(100, Math.max(0, Math.round(directPercent)));
|
|
38
|
+
}
|
|
39
|
+
const size = Number(item.size) || 0;
|
|
40
|
+
return size > 0 && Number(item.bytesSent) > 0
|
|
41
|
+
? Math.min(100, Math.max(0, Math.round(((Number(item.bytesSent) || 0) / size) * 100)))
|
|
42
|
+
: null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function itemProgressVisible(item = {}) {
|
|
46
|
+
return activePhases.has(itemPhase(item));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function itemProgressIndeterminate(item = {}) {
|
|
50
|
+
return itemProgressVisible(item) && itemPercent(item) === null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function itemProgressValue(item = {}) {
|
|
54
|
+
return itemPercent(item) ?? 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function itemBytesLabel(item = {}) {
|
|
58
|
+
const size = Math.max(0, Number(item.size) || 0);
|
|
59
|
+
const sent = Math.min(Math.max(0, Number(item.bytesSent) || 0), size);
|
|
60
|
+
const percent = itemPercent(item);
|
|
61
|
+
return percent === null
|
|
62
|
+
? t("jskit.assistant.attachments.bytes", { sent: attachmentSizeText(sent), size: attachmentSizeText(size) })
|
|
63
|
+
: t("jskit.assistant.attachments.bytesPercent", { sent: attachmentSizeText(sent), size: attachmentSizeText(size), percent });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function itemStatusLabel(item = {}) {
|
|
67
|
+
const labels = {
|
|
68
|
+
cancelled: "jskit.assistant.attachments.status.cancelled",
|
|
69
|
+
delivering: "jskit.assistant.attachments.status.delivering",
|
|
70
|
+
failed: item.failureStage === "preparing"
|
|
71
|
+
? "jskit.assistant.attachments.status.preparationFailed"
|
|
72
|
+
: item.failureStage === "handoff"
|
|
73
|
+
? "jskit.assistant.attachments.status.sendingFailed"
|
|
74
|
+
: "jskit.assistant.attachments.status.uploadFailed",
|
|
75
|
+
preparing: "jskit.assistant.attachments.status.preparing",
|
|
76
|
+
queued: "jskit.assistant.attachments.status.queued",
|
|
77
|
+
ready: "jskit.assistant.attachments.status.ready",
|
|
78
|
+
uploading: "jskit.assistant.attachments.status.uploading"
|
|
79
|
+
};
|
|
80
|
+
return t(Object.hasOwn(labels, itemPhase(item)) ? labels[itemPhase(item)] : "jskit.assistant.attachments.status.queued");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function itemCancelable(item = {}) {
|
|
84
|
+
return ["preparing", "queued", "uploading"].includes(itemPhase(item));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function itemIcon(item = {}) {
|
|
88
|
+
if (itemPhase(item) === "ready") {
|
|
89
|
+
return mdiCheckCircleOutline;
|
|
90
|
+
}
|
|
91
|
+
if (itemPhase(item) === "failed") {
|
|
92
|
+
return mdiAlertCircleOutline;
|
|
93
|
+
}
|
|
94
|
+
return mdiFileOutline;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function itemColor(item = {}) {
|
|
98
|
+
if (itemPhase(item) === "ready") {
|
|
99
|
+
return "success";
|
|
100
|
+
}
|
|
101
|
+
if (itemPhase(item) === "failed") {
|
|
102
|
+
return "error";
|
|
103
|
+
}
|
|
104
|
+
return "primary";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
activePhases,
|
|
109
|
+
itemBytesLabel,
|
|
110
|
+
itemCancelable,
|
|
111
|
+
itemColor,
|
|
112
|
+
itemIcon,
|
|
113
|
+
itemKey,
|
|
114
|
+
itemName,
|
|
115
|
+
itemPercent,
|
|
116
|
+
itemPhase,
|
|
117
|
+
itemProgressIndeterminate,
|
|
118
|
+
itemProgressValue,
|
|
119
|
+
itemProgressVisible,
|
|
120
|
+
itemStatusLabel
|
|
121
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { reactive } from "vue";
|
|
2
|
+
import { useJskitText } from "../i18n.js";
|
|
3
|
+
|
|
4
|
+
const { t } = useJskitText();
|
|
5
|
+
|
|
6
|
+
function attachmentUploadError(attachment) {
|
|
7
|
+
return attachment?.error || attachment?.errors?.[0]?.message || "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function attachmentIdentity(attachment) {
|
|
11
|
+
return String(
|
|
12
|
+
attachment?.clientId ||
|
|
13
|
+
attachment?.attachmentId ||
|
|
14
|
+
attachment?.path ||
|
|
15
|
+
attachment?.fileName ||
|
|
16
|
+
""
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// readable-check: allow module-state - shared resource: every queue on the page draws client ids from one sequence, so no two queues hand out the same id
|
|
21
|
+
let attachmentClientSequence = 0;
|
|
22
|
+
|
|
23
|
+
function nextAttachmentClientId() {
|
|
24
|
+
attachmentClientSequence += 1;
|
|
25
|
+
return `attachment-${Date.now().toString(36)}-${attachmentClientSequence.toString(36)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function attachmentFileName(file) {
|
|
29
|
+
return String(file?.name || "attachment");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The upload's promise and its cancel, whichever of the documented shapes the application's upload returned. */
|
|
33
|
+
// readable-check: allow method-probe - input validation: an application's upload returns a promise, or { promise } with abort() or cancel()
|
|
34
|
+
function attachmentUploadResult(upload = null) {
|
|
35
|
+
if (upload && typeof upload.then === "function") {
|
|
36
|
+
return {
|
|
37
|
+
cancel: typeof upload.cancel === "function" ? () => upload.cancel() : null,
|
|
38
|
+
promise: upload
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (upload?.promise && typeof upload.promise.then === "function") {
|
|
42
|
+
const cancel = typeof upload.abort === "function"
|
|
43
|
+
? () => upload.abort()
|
|
44
|
+
: typeof upload.cancel === "function"
|
|
45
|
+
? () => upload.cancel()
|
|
46
|
+
: null;
|
|
47
|
+
return {
|
|
48
|
+
cancel,
|
|
49
|
+
promise: upload.promise
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
cancel: null,
|
|
54
|
+
promise: Promise.resolve(upload)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function attachmentProgress(progress = {}, possibleTotal, fallbackTotal = 0) {
|
|
59
|
+
const directNumericProgress = typeof progress === "number" ? progress : null;
|
|
60
|
+
const progressObject = progress && typeof progress === "object" ? progress : {};
|
|
61
|
+
const totalBytes = Math.max(0, Number(
|
|
62
|
+
progressObject.totalBytes ??
|
|
63
|
+
progressObject.total ??
|
|
64
|
+
possibleTotal ??
|
|
65
|
+
fallbackTotal
|
|
66
|
+
) || 0);
|
|
67
|
+
const bytesValue = progressObject.bytesSent ??
|
|
68
|
+
progressObject.loaded ??
|
|
69
|
+
progressObject.uploadedBytes ??
|
|
70
|
+
directNumericProgress;
|
|
71
|
+
const bytesKnown = bytesValue !== null && bytesValue !== undefined && bytesValue !== "";
|
|
72
|
+
let bytesSent = bytesKnown ? Number(bytesValue) || 0 : 0;
|
|
73
|
+
const directPercent = progressObject.percent;
|
|
74
|
+
let percent = directPercent !== null && directPercent !== undefined && directPercent !== ""
|
|
75
|
+
? Number(directPercent)
|
|
76
|
+
: null;
|
|
77
|
+
const progressValue = progressObject.progress;
|
|
78
|
+
if (!Number.isFinite(percent) && progressValue !== null && progressValue !== undefined && progressValue !== "") {
|
|
79
|
+
const numericProgressValue = Number(progressValue);
|
|
80
|
+
if (Number.isFinite(numericProgressValue)) {
|
|
81
|
+
percent = numericProgressValue <= 1 ? numericProgressValue * 100 : numericProgressValue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!Number.isFinite(percent)) {
|
|
85
|
+
percent = bytesKnown && totalBytes > 0 ? (bytesSent / totalBytes) * 100 : null;
|
|
86
|
+
}
|
|
87
|
+
if (percent !== null) {
|
|
88
|
+
percent = Math.max(0, Math.min(100, percent));
|
|
89
|
+
}
|
|
90
|
+
if (totalBytes > 0) {
|
|
91
|
+
const inferredBytes = percent === null ? 0 : totalBytes * percent / 100;
|
|
92
|
+
bytesSent = Math.max(0, Math.min(totalBytes, bytesSent || inferredBytes));
|
|
93
|
+
} else {
|
|
94
|
+
bytesSent = Math.max(0, bytesSent);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
bytesSent,
|
|
98
|
+
percent,
|
|
99
|
+
totalBytes
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function attachmentReceipt(row, uploaded) {
|
|
104
|
+
return {
|
|
105
|
+
...uploaded,
|
|
106
|
+
attachmentId: String(uploaded?.attachmentId || ""),
|
|
107
|
+
clientId: row.clientId,
|
|
108
|
+
fileName: String(uploaded?.fileName || row.fileName),
|
|
109
|
+
sessionId: row.sessionId,
|
|
110
|
+
size: Number(uploaded?.size ?? row.size) || 0
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createWaiter(row) {
|
|
115
|
+
let resolve = null;
|
|
116
|
+
const promise = new Promise((currentResolve) => {
|
|
117
|
+
resolve = currentResolve;
|
|
118
|
+
});
|
|
119
|
+
row.waiter = {
|
|
120
|
+
promise,
|
|
121
|
+
resolve,
|
|
122
|
+
settled: false
|
|
123
|
+
};
|
|
124
|
+
return row.waiter;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function settleWaiter(row, value = null, waiter = row.waiter) {
|
|
128
|
+
if (!waiter || waiter.settled) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
waiter.settled = true;
|
|
132
|
+
waiter.resolve(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
function updateRowProgress(row, progress, total) {
|
|
137
|
+
if (!row.retained || row.phase !== "uploading") {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const next = attachmentProgress(progress, total, row.totalBytes);
|
|
141
|
+
row.bytesSent = Math.max(row.bytesSent, next.bytesSent);
|
|
142
|
+
row.totalBytes = next.totalBytes || row.totalBytes;
|
|
143
|
+
if (next.percent !== null) {
|
|
144
|
+
row.progress = Math.max(Number(row.progress) || 0, next.percent);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A new queue row for one file, or for a file a producer is still preparing. */
|
|
149
|
+
function buildQueueRow({
|
|
150
|
+
currentSessionId,
|
|
151
|
+
file = null,
|
|
152
|
+
fileName = "attachment",
|
|
153
|
+
phase = "queued",
|
|
154
|
+
producer = null,
|
|
155
|
+
size = 0
|
|
156
|
+
} = {}) {
|
|
157
|
+
const normalizedSize = Number(file?.size ?? size) || 0;
|
|
158
|
+
return reactive({
|
|
159
|
+
activeHandoffToken: null,
|
|
160
|
+
activeToken: null,
|
|
161
|
+
attachmentId: "",
|
|
162
|
+
bytesSent: 0,
|
|
163
|
+
clientId: nextAttachmentClientId(),
|
|
164
|
+
error: "",
|
|
165
|
+
failureStage: "",
|
|
166
|
+
file,
|
|
167
|
+
fileName: file ? attachmentFileName(file) : String(fileName || "attachment"),
|
|
168
|
+
path: "",
|
|
169
|
+
phase,
|
|
170
|
+
producer,
|
|
171
|
+
producerAbortController: null,
|
|
172
|
+
producerToken: null,
|
|
173
|
+
progress: null,
|
|
174
|
+
receipt: null,
|
|
175
|
+
retained: true,
|
|
176
|
+
sessionId: currentSessionId,
|
|
177
|
+
size: normalizedSize,
|
|
178
|
+
totalBytes: normalizedSize,
|
|
179
|
+
waiter: null
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Deletes an uploaded file that no message will send, once per receipt, and waits for the deletions under way.
|
|
185
|
+
* A failed deletion is reported and resolves false.
|
|
186
|
+
* @param {{
|
|
187
|
+
* deleteAttachment?: import("../types.js").AssistantAttachmentDeletion,
|
|
188
|
+
* deletesUnsentUploads: boolean,
|
|
189
|
+
* reportAttachmentError: (error: unknown, fallbackMessage: string) => string
|
|
190
|
+
* }} dependencies
|
|
191
|
+
*/
|
|
192
|
+
function createReceiptCleanup({ deleteAttachment, deletesUnsentUploads, reportAttachmentError }) {
|
|
193
|
+
const cleanupTasks = new Set();
|
|
194
|
+
const receiptCleanupById = new Map();
|
|
195
|
+
const cleanedReceiptIds = new Set();
|
|
196
|
+
|
|
197
|
+
function trackCleanup(task) {
|
|
198
|
+
const cleanup = Promise.resolve(task).catch((error) => {
|
|
199
|
+
reportAttachmentError(error, t("jskit.assistant.attachments.error.cleanupFailed"));
|
|
200
|
+
return false;
|
|
201
|
+
});
|
|
202
|
+
cleanupTasks.add(cleanup);
|
|
203
|
+
cleanup.finally(() => cleanupTasks.delete(cleanup));
|
|
204
|
+
return cleanup;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function deleteUploadedAttachment(currentSessionId, attachment) {
|
|
208
|
+
const attachmentId = String(attachment?.attachmentId || "").trim();
|
|
209
|
+
if (!attachmentId || !deletesUnsentUploads) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const response = await deleteAttachment(currentSessionId, attachmentId);
|
|
213
|
+
if (response?.ok === false) {
|
|
214
|
+
throw new Error(attachmentUploadError(response) || t("jskit.assistant.attachments.error.cleanupFailed"));
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function cleanupReceipt(currentSessionId, receipt) {
|
|
220
|
+
const attachmentId = String(receipt?.attachmentId || "").trim();
|
|
221
|
+
const cleanupId = `${currentSessionId}:${attachmentId}`;
|
|
222
|
+
if (!attachmentId) {
|
|
223
|
+
return Promise.resolve(false);
|
|
224
|
+
}
|
|
225
|
+
if (cleanedReceiptIds.has(cleanupId)) {
|
|
226
|
+
return Promise.resolve(false);
|
|
227
|
+
}
|
|
228
|
+
if (receiptCleanupById.has(cleanupId)) {
|
|
229
|
+
return receiptCleanupById.get(cleanupId);
|
|
230
|
+
}
|
|
231
|
+
const cleanup = trackCleanup(deleteUploadedAttachment(currentSessionId, receipt)).then((deleted) => {
|
|
232
|
+
if (deleted) {
|
|
233
|
+
cleanedReceiptIds.add(cleanupId);
|
|
234
|
+
}
|
|
235
|
+
return deleted;
|
|
236
|
+
});
|
|
237
|
+
receiptCleanupById.set(cleanupId, cleanup);
|
|
238
|
+
cleanup.finally(() => receiptCleanupById.delete(cleanupId));
|
|
239
|
+
return cleanup;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function waitForAttachmentCleanup() {
|
|
243
|
+
while (cleanupTasks.size > 0) {
|
|
244
|
+
await Promise.allSettled([...cleanupTasks]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return Object.freeze({
|
|
249
|
+
cleanupReceipt,
|
|
250
|
+
waitForAttachmentCleanup
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export {
|
|
255
|
+
attachmentFileName,
|
|
256
|
+
attachmentIdentity,
|
|
257
|
+
attachmentReceipt,
|
|
258
|
+
attachmentUploadError,
|
|
259
|
+
attachmentUploadResult,
|
|
260
|
+
buildQueueRow,
|
|
261
|
+
createReceiptCleanup,
|
|
262
|
+
createWaiter,
|
|
263
|
+
settleWaiter,
|
|
264
|
+
updateRowProgress
|
|
265
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// What the transcript shows of a conversation: its saved turns, with the reply still streaming, the delivery's
|
|
2
|
+
// unconfirmed messages and the application's unsent previews laid over them.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The transcript's props for an adapter's conversation.
|
|
6
|
+
* @param {import("../types.js").AssistantConversationAdapter} adapter
|
|
7
|
+
*/
|
|
8
|
+
function displayedConversation(adapter) {
|
|
9
|
+
const saved = adapter.conversation;
|
|
10
|
+
if (!adapter.delivery && !saved.previewMessage && !saved.previewMessages?.length && !saved.interimReply) return saved;
|
|
11
|
+
const { previewMessage, previewMessages, interimReply, ...display } = saved;
|
|
12
|
+
let turns = adapter.delivery ? adapter.delivery.turns(saved.turns || []) : saved.turns || [];
|
|
13
|
+
if (interimReply?.text && interimReply.turnId) {
|
|
14
|
+
const assistant = { ...interimReply, messageId: interimReply.id, role: "assistant" };
|
|
15
|
+
turns = turns.map(turn => turn.turnId !== interimReply.turnId ? turn : {
|
|
16
|
+
...turn, assistant,
|
|
17
|
+
commentary: (turn.commentary || []).filter(message =>
|
|
18
|
+
(message.outputId || message.messageId) !== (interimReply.outputId || interimReply.id)),
|
|
19
|
+
...(Array.isArray(turn.messages) ? { messages: turn.messages.filter(message =>
|
|
20
|
+
message.role !== "assistant" &&
|
|
21
|
+
(message.outputId || message.messageId) !== (interimReply.outputId || interimReply.id)).concat(assistant) } : {})
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
// This list projects existing capture/pending owners; it never queues delivery.
|
|
25
|
+
const previews = Array.isArray(previewMessages) ? previewMessages : previewMessage ? [previewMessage] : [];
|
|
26
|
+
for (const preview of previews) {
|
|
27
|
+
const messageId = String(preview?.messageId || preview?.id || "").trim();
|
|
28
|
+
const localDelivery = messageId && adapter.delivery?.find(messageId);
|
|
29
|
+
if ((!localDelivery || localDelivery.status === "failed") && preview?.actions?.send) {
|
|
30
|
+
// Keep one unadmitted bubble and its real status until explicit Edit/discard.
|
|
31
|
+
// A receipt:false user row is not acknowledgement of the pending words.
|
|
32
|
+
turns = turns.map(turn => {
|
|
33
|
+
const failed = turn.optimistic?.id === messageId && turn.optimistic.status === "failed";
|
|
34
|
+
const unadmitted = turn.user?.receipt === false && String(turn.user.messageId || turn.user.id || "") === messageId;
|
|
35
|
+
if (!failed && !unadmitted) return turn;
|
|
36
|
+
return { ...turn, preview: true, previewMessage: preview, user: { ...turn.user, text: String(preview.text || "") } };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const alreadyShown = messageId && (localDelivery || turns.some(turn => [
|
|
40
|
+
turn.system, turn.user, turn.assistant, ...(turn.thinking || []), ...(turn.commentary || []), ...(turn.messages || [])
|
|
41
|
+
].some(message => String(message?.messageId || message?.id || "") === messageId &&
|
|
42
|
+
(message.receipt !== false || turn.preview))));
|
|
43
|
+
if (messageId && (preview?.text || preview?.actions?.editing) && !alreadyShown) {
|
|
44
|
+
const user = { messageId, role: "user", text: String(preview.text || "") };
|
|
45
|
+
turns = [...turns, { turnId: messageId, user, messages: [user], preview: true, previewMessage: preview,
|
|
46
|
+
optimistic: { id: messageId, status: "pending" } }];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Transcription and interim replies are display only. The receipt watcher
|
|
50
|
+
// receives canonical turns, never these presentation or delivery overlays.
|
|
51
|
+
return { ...display, turns, welcomeMessage: turns.length ? "" : saved.welcomeMessage };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { displayedConversation };
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { reactive, toRaw, toValue, watch } from "vue";
|
|
2
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
3
|
import { useJskitText } from "../i18n.js";
|
|
3
4
|
|
|
4
5
|
const { serverMessage, t } = useJskitText();
|
|
5
6
|
|
|
6
|
-
const messageText = value => String(value || "").trim();
|
|
7
|
-
|
|
8
7
|
function turnMatchesOptimisticMessage(turn = {}, optimistic = {}) {
|
|
9
8
|
const authored = optimistic.origin === "application" ? turn.system : turn.user;
|
|
10
|
-
const canonicalMessageId =
|
|
11
|
-
const optimisticMessageId =
|
|
9
|
+
const canonicalMessageId = normalizeText(authored?.messageId);
|
|
10
|
+
const optimisticMessageId = normalizeText(optimistic.id);
|
|
12
11
|
if (canonicalMessageId && optimisticMessageId) {
|
|
13
12
|
return canonicalMessageId === optimisticMessageId;
|
|
14
13
|
}
|
|
15
|
-
if (
|
|
14
|
+
if (normalizeText(authored?.text) !== optimistic.text) {
|
|
16
15
|
return false;
|
|
17
16
|
}
|
|
18
17
|
const userAtMs = Date.parse(String(authored?.at || ""));
|
|
@@ -36,8 +35,11 @@ function unmatchedOptimisticMessages(turns = [], optimisticMessages = [], { rece
|
|
|
36
35
|
});
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
/**
|
|
39
|
+
* One delivery state per conversation. The caller owns transport and receipts; the shared assistant element renders
|
|
40
|
+
* pending, failed and uncertain turns. A delivery made without `deliver` takes one with each `send()`.
|
|
41
|
+
* @param {{ deliver?: (payload: object) => unknown }} [options]
|
|
42
|
+
*/
|
|
41
43
|
function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
42
44
|
const state = reactive({ messages: [], sending: false });
|
|
43
45
|
const acceptanceCallbacks = new WeakMap();
|
|
@@ -118,7 +120,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
118
120
|
queue = false
|
|
119
121
|
} = {}) {
|
|
120
122
|
if ((state.sending && !queue) || pendingSends.has(messageId) || find(messageId)?.status === "uncertain" ||
|
|
121
|
-
find(messageId)?.cancelling || (!
|
|
123
|
+
find(messageId)?.cancelling || (!normalizeText(payload?.message) && !payload?.displayAttachments?.length)) return false;
|
|
122
124
|
if (typeof deliver !== "function") throw new TypeError("Message delivery requires a deliver function.");
|
|
123
125
|
const snapshot = JSON.parse(JSON.stringify(payload));
|
|
124
126
|
const previous = find(messageId);
|
|
@@ -178,6 +180,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
178
180
|
fail(messageId, error, uncertainOnError || error?.status === "uncertain" ? "uncertain" : "failed");
|
|
179
181
|
throw error;
|
|
180
182
|
} finally {
|
|
183
|
+
// readable-check: allow optional-call - optional operation: a send without receiptTurns starts no receipt watcher
|
|
181
184
|
stopReceipt?.();
|
|
182
185
|
ownerPendingSends.delete(messageId);
|
|
183
186
|
if (pendingSends === ownerPendingSends) {
|
|
@@ -206,6 +209,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
206
209
|
const entry = toRaw(message);
|
|
207
210
|
const acknowledge = acceptanceCallbacks.get(entry);
|
|
208
211
|
acceptanceCallbacks.delete(entry);
|
|
212
|
+
// readable-check: allow optional-call - optional operation: a message restored from history has no acceptance callback
|
|
209
213
|
acknowledge?.();
|
|
210
214
|
}
|
|
211
215
|
}
|
|
@@ -223,7 +227,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
|
|
|
223
227
|
}
|
|
224
228
|
|
|
225
229
|
function restoreUncertain(request, savedTurns = []) {
|
|
226
|
-
const messageId =
|
|
230
|
+
const messageId = normalizeText(request?.messageId);
|
|
227
231
|
if (!messageId || savedTurns.some(turn => (turn.user || turn.system)?.messageId === messageId &&
|
|
228
232
|
(turn.user || turn.system).receipt !== false) ||
|
|
229
233
|
find(messageId)?.status === "accepted") return false;
|