@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
@@ -2,7 +2,6 @@ import {
2
2
  computed,
3
3
  getCurrentScope,
4
4
  onScopeDispose,
5
- reactive,
6
5
  ref,
7
6
  toValue,
8
7
  watch
@@ -10,6 +9,26 @@ import {
10
9
  import { ASSISTANT_ATTACHMENT_MAX_ITEMS } from "../../shared/conversation/attachments.js";
11
10
  import { useJskitText } from "../i18n.js";
12
11
  import { attachmentLimitText } from "./attachmentSize.js";
12
+ import {
13
+ assistantAttachmentEventHasFiles,
14
+ assistantAttachmentFiles,
15
+ assistantAttachmentFilesFromDropEvent,
16
+ assistantAttachmentFilesFromPasteEvent,
17
+ assistantAttachmentFilesFromTransferItems,
18
+ useAttachmentDropTarget
19
+ } from "./attachmentFiles.js";
20
+ import {
21
+ attachmentFileName,
22
+ attachmentIdentity,
23
+ attachmentReceipt,
24
+ attachmentUploadError,
25
+ attachmentUploadResult,
26
+ buildQueueRow,
27
+ createReceiptCleanup,
28
+ createWaiter,
29
+ settleWaiter,
30
+ updateRowProgress
31
+ } from "./attachmentUploadRows.js";
13
32
 
14
33
  const { serverMessage, t } = useJskitText();
15
34
 
@@ -27,91 +46,6 @@ function assistantAttachmentUploadAllowed(canUpload) {
27
46
  return toValue(canUpload) !== false;
28
47
  }
29
48
 
30
- function assistantAttachmentFiles(fileList = []) {
31
- return Array.from(fileList || []).filter((file) => file && file.size >= 0);
32
- }
33
-
34
- function assistantAttachmentFilesFromTransferItems(items = []) {
35
- return assistantAttachmentFiles(Array.from(items || [])
36
- .map((item) => {
37
- if (item?.kind !== "file" || typeof item.getAsFile !== "function") {
38
- return null;
39
- }
40
- return item.getAsFile();
41
- }));
42
- }
43
-
44
- function assistantAttachmentFilesFromDropEvent(event) {
45
- const dataTransfer = event?.dataTransfer;
46
- const itemFiles = assistantAttachmentFilesFromTransferItems(dataTransfer?.items);
47
- if (itemFiles.length > 0) {
48
- return itemFiles;
49
- }
50
- return assistantAttachmentFiles(dataTransfer?.files);
51
- }
52
-
53
- function assistantAttachmentFilesFromPasteEvent(event) {
54
- const clipboardData = event?.clipboardData;
55
- const itemFiles = assistantAttachmentFilesFromTransferItems(clipboardData?.items);
56
- if (itemFiles.length > 0) {
57
- return itemFiles;
58
- }
59
- return assistantAttachmentFiles(clipboardData?.files);
60
- }
61
-
62
- function assistantAttachmentEventHasFiles(event) {
63
- const dataTransfer = event?.dataTransfer;
64
- return assistantAttachmentFilesFromDropEvent(event).length > 0 ||
65
- Array.from(dataTransfer?.items || []).some((item) => item?.kind === "file") ||
66
- Array.from(dataTransfer?.types || []).some((type) => (
67
- type === "Files" ||
68
- type === "application/x-moz-file"
69
- ));
70
- }
71
-
72
- function assistantAttachmentClipboardText(event) {
73
- if (typeof event?.clipboardData?.getData !== "function") {
74
- return "";
75
- }
76
- return String(event.clipboardData.getData("text/plain") || "");
77
- }
78
-
79
- function assistantAttachmentClipboardLocalFileReference(event) {
80
- const clipboardData = event?.clipboardData;
81
- if (!clipboardData || typeof clipboardData.getData !== "function") {
82
- return "";
83
- }
84
- const types = Array.from(clipboardData.types || []);
85
- const text = [
86
- clipboardData.getData("x-special/gnome-copied-files"),
87
- clipboardData.getData("text/uri-list"),
88
- clipboardData.getData("text/plain")
89
- ].map((value) => String(value || "").trim()).filter(Boolean).join("\n");
90
- if (
91
- types.includes("Files") ||
92
- types.includes("application/x-moz-file") ||
93
- types.includes("x-special/gnome-copied-files") ||
94
- /^file:/imu.test(text)
95
- ) {
96
- return text || "file://";
97
- }
98
- return "";
99
- }
100
-
101
- function attachmentUploadError(attachment = {}) {
102
- return attachment?.error || attachment?.errors?.[0]?.message || "";
103
- }
104
-
105
- function attachmentIdentity(attachment = {}) {
106
- return String(
107
- attachment?.clientId ||
108
- attachment?.attachmentId ||
109
- attachment?.path ||
110
- attachment?.fileName ||
111
- ""
112
- );
113
- }
114
-
115
49
  const ASSISTANT_ATTACHMENT_MAX_BYTES = 100_000_000;
116
50
  const ASSISTANT_ATTACHMENT_UPLOAD_CONCURRENCY = 2;
117
51
  const ASSISTANT_ATTACHMENT_UNRESOLVED_PHASES = new Set([
@@ -122,126 +56,53 @@ const ASSISTANT_ATTACHMENT_UNRESOLVED_PHASES = new Set([
122
56
  "failed"
123
57
  ]);
124
58
 
125
- let attachmentClientSequence = 0;
126
-
127
- function nextAttachmentClientId() {
128
- attachmentClientSequence += 1;
129
- return `attachment-${Date.now().toString(36)}-${attachmentClientSequence.toString(36)}`;
130
- }
131
-
132
- function attachmentFileName(file = {}) {
133
- return String(file?.name || "attachment");
134
- }
59
+ /** Without an onError, an attachment failure shows only in the queue's own status. */
60
+ function keepAttachmentErrorInStatus() {}
135
61
 
136
- function attachmentUploadResult(upload = null) {
137
- if (upload && typeof upload.then === "function") {
138
- return {
139
- cancel: typeof upload.cancel === "function" ? () => upload.cancel() : null,
140
- promise: upload
141
- };
142
- }
143
- if (upload?.promise && typeof upload.promise.then === "function") {
144
- const cancel = typeof upload.abort === "function"
145
- ? () => upload.abort()
146
- : typeof upload.cancel === "function"
147
- ? () => upload.cancel()
148
- : null;
149
- return {
150
- cancel,
151
- promise: upload.promise
152
- };
153
- }
154
- return {
155
- cancel: null,
156
- promise: Promise.resolve(upload)
157
- };
158
- }
159
-
160
- function attachmentProgress(progress = {}, possibleTotal, fallbackTotal = 0) {
161
- const directNumericProgress = typeof progress === "number" ? progress : null;
162
- const progressObject = progress && typeof progress === "object" ? progress : {};
163
- const totalBytes = Math.max(0, Number(
164
- progressObject.totalBytes ??
165
- progressObject.total ??
166
- possibleTotal ??
167
- fallbackTotal
168
- ) || 0);
169
- const bytesValue = progressObject.bytesSent ??
170
- progressObject.loaded ??
171
- progressObject.uploadedBytes ??
172
- directNumericProgress;
173
- const bytesKnown = bytesValue !== null && bytesValue !== undefined && bytesValue !== "";
174
- let bytesSent = bytesKnown ? Number(bytesValue) || 0 : 0;
175
- const directPercent = progressObject.percent;
176
- let percent = directPercent !== null && directPercent !== undefined && directPercent !== ""
177
- ? Number(directPercent)
178
- : null;
179
- const progressValue = progressObject.progress;
180
- if (!Number.isFinite(percent) && progressValue !== null && progressValue !== undefined && progressValue !== "") {
181
- const numericProgressValue = Number(progressValue);
182
- if (Number.isFinite(numericProgressValue)) {
183
- percent = numericProgressValue <= 1 ? numericProgressValue * 100 : numericProgressValue;
184
- }
185
- }
186
- if (!Number.isFinite(percent)) {
187
- percent = bytesKnown && totalBytes > 0 ? (bytesSent / totalBytes) * 100 : null;
188
- }
189
- if (percent !== null) {
190
- percent = Math.max(0, Math.min(100, percent));
191
- }
192
- if (totalBytes > 0) {
193
- const inferredBytes = percent === null ? 0 : totalBytes * percent / 100;
194
- bytesSent = Math.max(0, Math.min(totalBytes, bytesSent || inferredBytes));
195
- } else {
196
- bytesSent = Math.max(0, bytesSent);
197
- }
198
- return {
199
- bytesSent,
200
- percent,
201
- totalBytes
202
- };
203
- }
204
-
205
- function attachmentReceipt(row, uploaded = {}) {
206
- return {
207
- ...uploaded,
208
- attachmentId: String(uploaded?.attachmentId || ""),
209
- clientId: row.clientId,
210
- fileName: String(uploaded?.fileName || row.fileName),
211
- sessionId: row.sessionId,
212
- size: Number(uploaded?.size ?? row.size) || 0
213
- };
62
+ /** Without an onUploaded, an uploaded file stays in the queue, ready to send with the next message. */
63
+ async function keepUploadedAttachmentsReady() {
64
+ return null;
214
65
  }
215
66
 
67
+ /**
68
+ * Attachment upload and selection state for one composer: a bounded queue that uploads files a few at a time, hands
69
+ * each receipt to `onUploaded`, and deletes uploaded files no message will send. The application owns storage and
70
+ * authorization through `uploadAttachment` and `deleteAttachment`.
71
+ * @param {{
72
+ * canUpload?: import("vue").MaybeRefOrGetter<boolean>,
73
+ * deleteAttachment?: import("../types.js").AssistantAttachmentDeletion,
74
+ * onError?: (error: unknown, fallbackMessage: string) => void,
75
+ * onUploaded?: (receipts: import("../types.js").AssistantAttachmentReceipt[]) => Promise<{ accepted?: boolean } | null>,
76
+ * sessionId: import("vue").MaybeRefOrGetter<string>,
77
+ * uploadAttachment: import("../types.js").AssistantAttachmentUpload,
78
+ * maxBytes?: number,
79
+ * maxItems?: number,
80
+ * uploadConcurrency?: number
81
+ * }} options
82
+ */
216
83
  function useAssistantAttachments({
217
84
  canUpload = () => true,
218
- deleteAttachment = null,
219
- onError = null,
220
- onUploaded = async () => null,
85
+ deleteAttachment,
86
+ onError = keepAttachmentErrorInStatus,
87
+ onUploaded = keepUploadedAttachmentsReady,
221
88
  sessionId,
222
89
  uploadAttachment,
223
90
  maxBytes = ASSISTANT_ATTACHMENT_MAX_BYTES,
224
91
  maxItems = ASSISTANT_ATTACHMENT_MAX_ITEMS,
225
92
  uploadConcurrency = ASSISTANT_ATTACHMENT_UPLOAD_CONCURRENCY
226
- } = {}) {
93
+ }) {
227
94
  if (![maxBytes, maxItems, uploadConcurrency].every(value => Number.isSafeInteger(value) && value > 0)) {
228
95
  throw new TypeError("Attachment limits must be positive integers.");
229
96
  }
230
97
  const attachments = ref([]);
231
98
  const queueItems = ref([]);
232
- const dragDepth = ref(0);
233
99
  const status = ref("");
234
100
  const activeUploadCount = ref(0);
235
101
  const uploadQueue = [];
236
102
  const rowsById = new Map();
237
103
  const activeAttempts = new Map();
238
- const cleanupTasks = new Set();
239
- const receiptCleanupById = new Map();
240
- const cleanedReceiptIds = new Set();
241
104
  let disposed = false;
242
- let stopSessionWatch = null;
243
105
 
244
- const dragActive = computed(() => dragDepth.value > 0);
245
106
  const uploading = computed(() => (
246
107
  activeUploadCount.value > 0 || queueItems.value.some((item) => (
247
108
  ["preparing", "uploading", "delivering"].includes(item.phase)
@@ -259,8 +120,7 @@ function useAssistantAttachments({
259
120
  !disposed &&
260
121
  assistantAttachmentSessionId(sessionId) &&
261
122
  assistantAttachmentUploadAllowed(canUpload) &&
262
- !atCapacity.value &&
263
- typeof uploadAttachment === "function"
123
+ !atCapacity.value
264
124
  ));
265
125
  const aggregateProgress = computed(() => {
266
126
  const retainedItems = queueItems.value.filter((item) => item.phase !== "cancelled");
@@ -281,7 +141,7 @@ function useAssistantAttachments({
281
141
  } = {}) {
282
142
  const message = attachmentErrorText(error, fallbackMessage);
283
143
  status.value = message;
284
- if (notify && typeof onError === "function") {
144
+ if (notify) {
285
145
  try {
286
146
  onError(error || new Error(message), fallbackMessage);
287
147
  } catch {
@@ -291,33 +151,16 @@ function useAssistantAttachments({
291
151
  return message;
292
152
  }
293
153
 
294
- function resetDragState() {
295
- dragDepth.value = 0;
296
- }
297
-
298
154
  function clearStatus() {
299
155
  status.value = "";
300
156
  }
301
157
 
302
- function createWaiter(row) {
303
- let resolve = null;
304
- const promise = new Promise((currentResolve) => {
305
- resolve = currentResolve;
306
- });
307
- row.waiter = {
308
- promise,
309
- resolve,
310
- settled: false
311
- };
312
- return row.waiter;
313
- }
314
-
315
- function settleWaiter(row, value = null, waiter = row.waiter) {
316
- if (!waiter || waiter.settled) {
317
- return;
318
- }
319
- waiter.settled = true;
320
- waiter.resolve(value);
158
+ /** Marks a row failed at one stage with its error's text, which also becomes the queue's status. */
159
+ function failRow(row, failureStage, error, fallbackMessage) {
160
+ row.error = attachmentErrorText(error, fallbackMessage);
161
+ row.failureStage = failureStage;
162
+ row.phase = "failed";
163
+ reportAttachmentError(error, fallbackMessage, { notify: false });
321
164
  }
322
165
 
323
166
  function canonicalRow(attachment = {}) {
@@ -327,62 +170,18 @@ function useAssistantAttachments({
327
170
  )) : null;
328
171
  }
329
172
 
330
- function trackCleanup(task) {
331
- const cleanup = Promise.resolve(task).catch((error) => {
332
- reportAttachmentError(error, t("jskit.assistant.attachments.error.cleanupFailed"));
333
- return false;
334
- });
335
- cleanupTasks.add(cleanup);
336
- cleanup.finally(() => cleanupTasks.delete(cleanup));
337
- return cleanup;
338
- }
339
-
340
- async function deleteUploadedAttachment(currentSessionId, attachment = {}) {
341
- const attachmentId = String(attachment?.attachmentId || "").trim();
342
- if (!attachmentId || typeof deleteAttachment !== "function") {
343
- return false;
344
- }
345
- const response = await deleteAttachment(currentSessionId, attachmentId);
346
- if (response?.ok === false) {
347
- throw new Error(attachmentUploadError(response) || t("jskit.assistant.attachments.error.cleanupFailed"));
348
- }
349
- return true;
350
- }
351
-
352
- function cleanupReceipt(currentSessionId, receipt = {}) {
353
- const attachmentId = String(receipt?.attachmentId || "").trim();
354
- const cleanupId = `${currentSessionId}:${attachmentId}`;
355
- if (!attachmentId) {
356
- return Promise.resolve(false);
357
- }
358
- if (cleanedReceiptIds.has(cleanupId)) {
359
- return Promise.resolve(false);
360
- }
361
- if (receiptCleanupById.has(cleanupId)) {
362
- return receiptCleanupById.get(cleanupId);
363
- }
364
- const cleanup = trackCleanup(deleteUploadedAttachment(currentSessionId, receipt)).then((deleted) => {
365
- if (deleted) {
366
- cleanedReceiptIds.add(cleanupId);
367
- }
368
- return deleted;
369
- });
370
- receiptCleanupById.set(cleanupId, cleanup);
371
- cleanup.finally(() => receiptCleanupById.delete(cleanupId));
372
- return cleanup;
373
- }
374
-
375
- function updateProgress(row, progress, total) {
376
- if (!row.retained || row.phase !== "uploading") {
377
- return;
378
- }
379
- const next = attachmentProgress(progress, total, row.totalBytes);
380
- row.bytesSent = Math.max(row.bytesSent, next.bytesSent);
381
- row.totalBytes = next.totalBytes || row.totalBytes;
382
- if (next.percent !== null) {
383
- row.progress = Math.max(Number(row.progress) || 0, next.percent);
384
- }
385
- }
173
+ // readable-check: allow method-probe - optional operation: a queue whose uploaded files are kept passes no deleteAttachment, so an upload no message sends stays stored
174
+ const deletesUnsentUploads = typeof deleteAttachment === "function";
175
+ const { cleanupReceipt, waitForAttachmentCleanup } = createReceiptCleanup({ deleteAttachment, deletesUnsentUploads, reportAttachmentError });
176
+ const {
177
+ dragActive,
178
+ handleDragEnter,
179
+ handleDragLeave,
180
+ handleDragOver,
181
+ handleDrop,
182
+ handlePaste,
183
+ resetDragState
184
+ } = useAttachmentDropTarget({ uploadFiles, reportAttachmentError });
386
185
 
387
186
  function removeReadyReceipt(row) {
388
187
  const removed = attachments.value.filter((attachment) => (
@@ -434,11 +233,8 @@ function useAssistantAttachments({
434
233
  }
435
234
  row.activeHandoffToken = null;
436
235
  removeReadyReceipt(row);
437
- row.error = attachmentErrorText(error, t("jskit.assistant.attachments.error.unusable"));
438
- row.failureStage = "handoff";
439
- row.phase = "failed";
236
+ failRow(row, "handoff", error, t("jskit.assistant.attachments.error.unusable"));
440
237
  settleWaiter(row, null, waiter);
441
- reportAttachmentError(error, t("jskit.assistant.attachments.error.unusable"), { notify: false });
442
238
  return null;
443
239
  }
444
240
  }
@@ -480,20 +276,15 @@ function useAssistantAttachments({
480
276
  const error = uploadError || new Error(
481
277
  attachmentUploadError(uploaded) || t("jskit.assistant.attachments.error.uploadFailed")
482
278
  );
483
- row.error = attachmentErrorText(error, t("jskit.assistant.attachments.error.uploadFailed"));
484
- row.failureStage = "upload";
485
- row.phase = "failed";
279
+ failRow(row, "upload", error, t("jskit.assistant.attachments.error.uploadFailed"));
486
280
  settleWaiter(row, null, attempt.waiter);
487
- reportAttachmentError(error, t("jskit.assistant.attachments.error.uploadFailed"), { notify: false });
488
281
  }
489
282
  pumpQueue();
490
283
  }
491
284
 
492
285
  function startUpload(row) {
493
286
  const token = Symbol(row.clientId);
494
- const abortController = typeof AbortController === "function"
495
- ? new AbortController()
496
- : null;
287
+ const abortController = new AbortController();
497
288
  const attempt = {
498
289
  abortController,
499
290
  cancel: null,
@@ -511,8 +302,8 @@ function useAssistantAttachments({
511
302
 
512
303
  try {
513
304
  const transport = attachmentUploadResult(uploadAttachment(row.sessionId, row.file, {
514
- onProgress: (progress, total) => updateProgress(row, progress, total),
515
- signal: abortController?.signal
305
+ onProgress: (progress, total) => updateRowProgress(row, progress, total),
306
+ signal: abortController.signal
516
307
  }));
517
308
  attempt.cancel = transport.cancel;
518
309
  attempt.settled = transport.promise.then(
@@ -558,7 +349,8 @@ function useAssistantAttachments({
558
349
  const attempt = token ? activeAttempts.get(token) : null;
559
350
  try {
560
351
  row.producerAbortController?.abort();
561
- attempt?.abortController?.abort();
352
+ attempt?.abortController.abort();
353
+ // readable-check: allow optional-call - optional operation: an upload that returns a bare promise has no cancel
562
354
  attempt?.cancel?.();
563
355
  } catch {
564
356
  // A cancelled transport is already detached; late success is still cleaned up.
@@ -576,8 +368,7 @@ function useAssistantAttachments({
576
368
  !row.retained ||
577
369
  !["failed", "cancelled"].includes(row.phase) ||
578
370
  row.sessionId !== assistantAttachmentSessionId(sessionId) ||
579
- !assistantAttachmentUploadAllowed(canUpload) ||
580
- typeof uploadAttachment !== "function"
371
+ !assistantAttachmentUploadAllowed(canUpload)
581
372
  ) {
582
373
  return Promise.resolve(null);
583
374
  }
@@ -598,10 +389,7 @@ function useAssistantAttachments({
598
389
  return waiter.promise;
599
390
  }
600
391
  if (row.size > maxBytes) {
601
- row.error = tooLarge(row);
602
- row.failureStage = "validation";
603
- row.phase = "failed";
604
- reportAttachmentError(row.error, t("jskit.assistant.attachments.error.uploadFailed"), { notify: false });
392
+ failRow(row, "validation", tooLarge(row), t("jskit.assistant.attachments.error.uploadFailed"));
605
393
  return Promise.resolve(null);
606
394
  }
607
395
  row.bytesSent = 0;
@@ -695,75 +483,18 @@ function useAssistantAttachments({
695
483
  return rows;
696
484
  }
697
485
 
698
- async function waitForAttachmentCleanup() {
699
- while (cleanupTasks.size > 0) {
700
- await Promise.allSettled([...cleanupTasks]);
701
- }
702
- }
703
-
704
486
  async function disposeAttachments() {
705
487
  if (disposed) {
706
488
  await waitForAttachmentCleanup();
707
489
  return [];
708
490
  }
709
491
  disposed = true;
710
- stopSessionWatch?.();
711
- stopSessionWatch = null;
492
+ stopSessionWatch();
712
493
  return abandonAttachments();
713
494
  }
714
495
 
715
- function handleDragEnter(event) {
716
- if (!assistantAttachmentEventHasFiles(event)) {
717
- return;
718
- }
719
- dragDepth.value += 1;
720
- if (event?.dataTransfer) {
721
- event.dataTransfer.dropEffect = "copy";
722
- }
723
- }
724
-
725
- function handleDragOver(event) {
726
- if (event?.dataTransfer) {
727
- event.dataTransfer.dropEffect = "copy";
728
- }
729
- }
730
-
731
- function handleDragLeave() {
732
- dragDepth.value = Math.max(0, dragDepth.value - 1);
733
- }
734
-
735
- function createQueueRow({
736
- currentSessionId,
737
- file = null,
738
- fileName = "attachment",
739
- phase = "queued",
740
- producer = null,
741
- size = 0
742
- } = {}) {
743
- const normalizedSize = Number(file?.size ?? size) || 0;
744
- const row = reactive({
745
- activeHandoffToken: null,
746
- activeToken: null,
747
- attachmentId: "",
748
- bytesSent: 0,
749
- clientId: nextAttachmentClientId(),
750
- error: "",
751
- failureStage: "",
752
- file,
753
- fileName: file ? attachmentFileName(file) : String(fileName || "attachment"),
754
- path: "",
755
- phase,
756
- producer,
757
- producerAbortController: null,
758
- producerToken: null,
759
- progress: null,
760
- receipt: null,
761
- retained: true,
762
- sessionId: currentSessionId,
763
- size: normalizedSize,
764
- totalBytes: normalizedSize,
765
- waiter: null
766
- });
496
+ function createQueueRow(options) {
497
+ const row = buildQueueRow(options);
767
498
  rowsById.set(row.clientId, row);
768
499
  queueItems.value.push(row);
769
500
  createWaiter(row);
@@ -782,11 +513,8 @@ function useAssistantAttachments({
782
513
  row.size = Number(preparedFile.size) || 0;
783
514
  row.totalBytes = row.size;
784
515
  if (row.size > maxBytes) {
785
- row.error = tooLarge(row);
786
- row.failureStage = "validation";
787
- row.phase = "failed";
516
+ failRow(row, "validation", tooLarge(row), t("jskit.assistant.attachments.error.uploadFailed"));
788
517
  settleWaiter(row, null, waiter);
789
- reportAttachmentError(row.error, t("jskit.assistant.attachments.error.uploadFailed"), { notify: false });
790
518
  return false;
791
519
  }
792
520
  row.phase = "queued";
@@ -798,9 +526,7 @@ function useAssistantAttachments({
798
526
  function startAttachmentProducer(row) {
799
527
  const token = Symbol(`${row.clientId}-producer`);
800
528
  const waiter = row.waiter;
801
- const abortController = typeof AbortController === "function"
802
- ? new AbortController()
803
- : null;
529
+ const abortController = new AbortController();
804
530
  row.file = null;
805
531
  row.producerAbortController = abortController;
806
532
  row.producerToken = token;
@@ -808,7 +534,7 @@ function useAssistantAttachments({
808
534
  row.error = "";
809
535
  row.failureStage = "";
810
536
  row.producerSettled = Promise.resolve().then(() => row.producer({
811
- signal: abortController?.signal
537
+ signal: abortController.signal
812
538
  })).then((file) => {
813
539
  if (row.producerToken !== token || !row.retained || disposed) {
814
540
  settleWaiter(row, null, waiter);
@@ -819,11 +545,8 @@ function useAssistantAttachments({
819
545
  try {
820
546
  queuePreparedFile(row, file, waiter);
821
547
  } catch (error) {
822
- row.error = attachmentErrorText(error, t("jskit.assistant.attachments.error.preparationFailed"));
823
- row.failureStage = "preparing";
824
- row.phase = "failed";
548
+ failRow(row, "preparing", error, t("jskit.assistant.attachments.error.preparationFailed"));
825
549
  settleWaiter(row, null, waiter);
826
- reportAttachmentError(error, t("jskit.assistant.attachments.error.preparationFailed"), { notify: false });
827
550
  }
828
551
  return file;
829
552
  }, (error) => {
@@ -837,10 +560,7 @@ function useAssistantAttachments({
837
560
  row.phase = "cancelled";
838
561
  row.error = "";
839
562
  } else {
840
- row.error = attachmentErrorText(error, t("jskit.assistant.attachments.error.preparationFailed"));
841
- row.failureStage = "preparing";
842
- row.phase = "failed";
843
- reportAttachmentError(error, t("jskit.assistant.attachments.error.preparationFailed"), { notify: false });
563
+ failRow(row, "preparing", error, t("jskit.assistant.attachments.error.preparationFailed"));
844
564
  }
845
565
  settleWaiter(row, null, waiter);
846
566
  return null;
@@ -852,6 +572,7 @@ function useAssistantAttachments({
852
572
  produce
853
573
  } = {}) {
854
574
  const currentSessionId = assistantAttachmentSessionId(sessionId);
575
+ // readable-check: allow method-probe - input validation: an application hands each call its file producer
855
576
  if (!canAddFiles.value || typeof produce !== "function") {
856
577
  if (atCapacity.value) {
857
578
  reportAttachmentError(tooMany());
@@ -877,8 +598,7 @@ function useAssistantAttachments({
877
598
  uploadableFiles.length < 1 ||
878
599
  !currentSessionId ||
879
600
  disposed ||
880
- !assistantAttachmentUploadAllowed(canUpload) ||
881
- typeof uploadAttachment !== "function"
601
+ !assistantAttachmentUploadAllowed(canUpload)
882
602
  ) {
883
603
  return [];
884
604
  }
@@ -896,11 +616,8 @@ function useAssistantAttachments({
896
616
  const waiter = row.waiter;
897
617
  waiters.push(waiter.promise);
898
618
  if (row.size > maxBytes) {
899
- row.error = tooLarge(row);
900
- row.failureStage = "validation";
901
- row.phase = "failed";
619
+ failRow(row, "validation", tooLarge(row), t("jskit.assistant.attachments.error.uploadFailed"));
902
620
  settleWaiter(row, null);
903
- reportAttachmentError(row.error, t("jskit.assistant.attachments.error.uploadFailed"), { notify: false });
904
621
  } else {
905
622
  uploadQueue.push(row);
906
623
  }
@@ -913,26 +630,7 @@ function useAssistantAttachments({
913
630
  ));
914
631
  }
915
632
 
916
- async function handleDrop(event) {
917
- resetDragState();
918
- return uploadFiles(assistantAttachmentFilesFromDropEvent(event));
919
- }
920
-
921
- async function handlePaste(event) {
922
- const files = assistantAttachmentFilesFromPasteEvent(event);
923
- if (files.length < 1) {
924
- if (assistantAttachmentClipboardLocalFileReference(event)) {
925
- reportAttachmentError(t("jskit.assistant.attachments.error.localFilePaste"));
926
- }
927
- return [];
928
- }
929
- if (!assistantAttachmentClipboardText(event)) {
930
- event?.preventDefault?.();
931
- }
932
- return uploadFiles(files);
933
- }
934
-
935
- stopSessionWatch = watch(
633
+ const stopSessionWatch = watch(
936
634
  () => assistantAttachmentSessionId(sessionId),
937
635
  (nextSessionId, previousSessionId) => {
938
636
  if (nextSessionId !== previousSessionId && previousSessionId) {