@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.
Files changed (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +13 -10
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. 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 = messageText(authored?.messageId);
11
- const optimisticMessageId = messageText(optimistic.id);
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 (messageText(authored?.text) !== optimistic.text) {
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
- // One delivery state per conversation. The caller owns transport and receipts;
40
- // the shared assistant element renders pending, failed and uncertain turns.
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 || (!messageText(payload?.message) && !payload?.displayAttachments?.length)) return false;
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 = messageText(request?.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;