@jskit-ai/assistant-core 0.2.6 → 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.
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 +18 -12
  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
@@ -8,14 +8,14 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.2.6",
12
- "@jskit-ai/http-runtime": "0.2.3",
13
- "@jskit-ai/kernel": "0.2.3",
11
+ "@jskit-ai/assistant-core": "0.2.8",
12
+ "@jskit-ai/http-runtime": "0.2.4",
13
+ "@jskit-ai/kernel": "0.2.4",
14
14
  "@mdi/js": "^7.4.47",
15
15
  "vue": "^3.5.13",
16
16
  "vuetify": "^4.0.0",
17
- "@jskit-ai/connectors-core": "0.2.3",
18
- "@jskit-ai/connectors-catalog": "0.2.3"
17
+ "@jskit-ai/connectors-core": "0.2.4",
18
+ "@jskit-ai/connectors-catalog": "0.2.4"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@vitejs/plugin-vue": "^6.0.7",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-core",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -8,6 +8,7 @@
8
8
  "exports": {
9
9
  "./client": "./src/client/index.js",
10
10
  "./server": "./src/server/index.js",
11
+ "./server/types": "./src/server/types.js",
11
12
  "./shared": "./src/shared/index.js",
12
13
  "./client/conversation": "./src/client/conversation/index.js",
13
14
  "./server/codex-events": "./src/server/hosts/codexEvents.js",
@@ -35,10 +36,11 @@
35
36
  "./testing/native-opencode": "./src/testing/nativeOpenCode.js",
36
37
  "./server/codex-client": "./src/server/conversation/codexClient.js",
37
38
  "./client/conversation-submit": "./src/client/conversation/submitText.js",
39
+ "./client/types": "./src/client/types.js",
38
40
  "./locales/en.json": "./locales/en.json"
39
41
  },
40
42
  "dependencies": {
41
- "@ai-sdk/anthropic": "^4.0.53",
43
+ "@ai-sdk/anthropic": "^4.0.93",
42
44
  "@ai-sdk/cerebras": "^3.0.48",
43
45
  "@ai-sdk/google": "^4.0.70",
44
46
  "@ai-sdk/groq": "^4.0.41",
@@ -48,18 +50,19 @@
48
50
  "@ai-sdk/perplexity": "^4.0.43",
49
51
  "@ai-sdk/togetherai": "^3.0.49",
50
52
  "@ai-sdk/xai": "^4.0.58",
51
- "@jskit-ai/resource-core": "0.1.194",
52
- "@jskit-ai/resource-crud-core": "0.1.194",
53
+ "@jskit-ai/agent": "0.1.0",
54
+ "@jskit-ai/resource-core": "0.1.195",
55
+ "@jskit-ai/resource-crud-core": "0.1.195",
53
56
  "@mdi/js": "^7.4.47",
54
57
  "@openrouter/ai-sdk-provider": "^3.0.0",
55
- "ai": "^7.0.101",
58
+ "ai": "^7.0.152",
56
59
  "json-rest-schema": "^1.0.17",
57
60
  "openai": "^6.22.0",
58
61
  "ws": "^8.20.1"
59
62
  },
60
63
  "peerDependencies": {
61
- "@jskit-ai/http-runtime": "0.2.3",
62
- "@jskit-ai/kernel": "0.2.3",
64
+ "@jskit-ai/http-runtime": "0.2.4",
65
+ "@jskit-ai/kernel": "0.2.4",
63
66
  "vue": "^3.5.13",
64
67
  "vuetify": "^4.0.0"
65
68
  },
@@ -123,6 +126,10 @@
123
126
  "subpath": "./server",
124
127
  "summary": "Framework integration: Exports reusable assistant AI/provider and NDJSON streaming helpers."
125
128
  },
129
+ {
130
+ "subpath": "./server/types",
131
+ "summary": "Framework integration: Types of the conversation runtime, tool catalogue and AI clients, and the contracts of what an application passes the runtime."
132
+ },
126
133
  {
127
134
  "subpath": "./shared",
128
135
  "summary": "Framework integration: Exports reusable assistant validators, path helpers, query keys, and stream/settings events."
@@ -198,6 +205,10 @@
198
205
  {
199
206
  "subpath": "./client/conversation-submit",
200
207
  "summary": "Optional UI: Original text submission helper forwards companion text through the application draft and canonical send action; also exported by ./client/conversation."
208
+ },
209
+ {
210
+ "subpath": "./client/types",
211
+ "summary": "Framework integration: Types of the conversation adapter, actions, attachments, suggestions, questions, message delivery and API client that client code receives."
201
212
  }
202
213
  ],
203
214
  "capabilities": []
@@ -0,0 +1,21 @@
1
+ // The browser's timers and animation frames, for client code that also runs where there is no window (server
2
+ // rendering and node tests). Without one, timers and frames are not scheduled and each caller takes its own path.
3
+
4
+ /** Whether the page has a window whose `name` (a timer, frame or media function) can be called. */
5
+ function hasWindowFunction(name) {
6
+ // readable-check: allow method-probe - platform object: window exists only in a browser
7
+ return typeof window !== "undefined" && typeof window[name] === "function";
8
+ }
9
+
10
+ /** Resolves after the next animation frame, or at once without a window. */
11
+ function waitForLayoutFrame() {
12
+ if (!hasWindowFunction("requestAnimationFrame")) {
13
+ return Promise.resolve();
14
+ }
15
+
16
+ return new Promise((resolve) => {
17
+ window.requestAnimationFrame(resolve);
18
+ });
19
+ }
20
+
21
+ export { hasWindowFunction, waitForLayoutFrame };
@@ -123,16 +123,29 @@
123
123
  <script setup>
124
124
  import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
125
125
  import {
126
- mdiAlertCircleOutline,
127
- mdiCheckCircleOutline,
128
126
  mdiClose,
129
127
  mdiCloseCircleOutline,
130
- mdiFileOutline,
131
128
  mdiRefresh
132
129
  } from "@mdi/js";
133
130
 
131
+ import { hasWindowFunction } from "../browser.js";
134
132
  import { useJskitText } from "../i18n.js";
135
133
  import { attachmentSizeText } from "./attachmentSize.js";
134
+ import {
135
+ activePhases,
136
+ itemBytesLabel,
137
+ itemCancelable,
138
+ itemColor,
139
+ itemIcon,
140
+ itemKey,
141
+ itemName,
142
+ itemPercent,
143
+ itemPhase,
144
+ itemProgressIndeterminate,
145
+ itemProgressValue,
146
+ itemProgressVisible,
147
+ itemStatusLabel
148
+ } from "./attachmentQueueItems.js";
136
149
 
137
150
  const { t } = useJskitText();
138
151
 
@@ -154,7 +167,6 @@ const props = defineProps({
154
167
  }
155
168
  });
156
169
 
157
- const activePhases = new Set(["queued", "preparing", "uploading", "delivering"]);
158
170
  const queueRoot = ref(null);
159
171
  const reducedMotion = ref(false);
160
172
  const actionElements = new Map();
@@ -221,93 +233,16 @@ const announcement = computed(() => {
221
233
  return messages.join(" ");
222
234
  });
223
235
 
224
- function itemKey(item = {}) {
225
- return String(item.clientId || item.id || item.attachmentId || item.fileName || "attachment");
226
- }
227
-
228
- function itemName(item = {}) {
229
- return String(item.fileName || item.file?.name || t("jskit.assistant.attachments.unnamed"));
230
- }
231
-
232
- function itemPhase(item = {}) {
233
- return String(item.phase || item.status || "queued");
234
- }
235
-
236
- function itemPercent(item = {}) {
237
- if (itemPhase(item) === "ready") {
238
- return 100;
239
- }
240
- if (itemPhase(item) === "delivering") {
241
- return 100;
242
- }
243
- if (itemPhase(item) === "queued") {
244
- return 0;
245
- }
246
- const progressValue = item.percent ?? item.progress;
247
- const directPercent = progressValue === null || progressValue === undefined || progressValue === ""
248
- ? null
249
- : Number(progressValue);
250
- if (directPercent !== null && Number.isFinite(directPercent)) {
251
- return Math.min(100, Math.max(0, Math.round(directPercent)));
252
- }
253
- const size = Number(item.size) || 0;
254
- return size > 0 && Number(item.bytesSent) > 0
255
- ? Math.min(100, Math.max(0, Math.round(((Number(item.bytesSent) || 0) / size) * 100)))
256
- : null;
257
- }
258
-
259
- function itemProgressVisible(item = {}) {
260
- return activePhases.has(itemPhase(item));
261
- }
262
-
263
- function itemProgressIndeterminate(item = {}) {
264
- return itemProgressVisible(item) && itemPercent(item) === null;
265
- }
266
-
267
- function itemProgressValue(item = {}) {
268
- return itemPercent(item) ?? 0;
269
- }
270
-
271
236
  function itemProgressStationary(item = {}) {
272
237
  return reducedMotion.value && itemProgressVisible(item) && itemPercent(item) === null;
273
238
  }
274
239
 
275
- function itemBytesLabel(item = {}) {
276
- const size = Math.max(0, Number(item.size) || 0);
277
- const sent = Math.min(Math.max(0, Number(item.bytesSent) || 0), size);
278
- const percent = itemPercent(item);
279
- return percent === null
280
- ? t("jskit.assistant.attachments.bytes", { sent: attachmentSizeText(sent), size: attachmentSizeText(size) })
281
- : t("jskit.assistant.attachments.bytesPercent", { sent: attachmentSizeText(sent), size: attachmentSizeText(size), percent });
282
- }
283
-
284
- function itemStatusLabel(item = {}) {
285
- const labels = {
286
- cancelled: "jskit.assistant.attachments.status.cancelled",
287
- delivering: "jskit.assistant.attachments.status.delivering",
288
- failed: item.failureStage === "preparing"
289
- ? "jskit.assistant.attachments.status.preparationFailed"
290
- : item.failureStage === "handoff"
291
- ? "jskit.assistant.attachments.status.sendingFailed"
292
- : "jskit.assistant.attachments.status.uploadFailed",
293
- preparing: "jskit.assistant.attachments.status.preparing",
294
- queued: "jskit.assistant.attachments.status.queued",
295
- ready: "jskit.assistant.attachments.status.ready",
296
- uploading: "jskit.assistant.attachments.status.uploading"
297
- };
298
- return t(Object.hasOwn(labels, itemPhase(item)) ? labels[itemPhase(item)] : "jskit.assistant.attachments.status.queued");
299
- }
300
-
301
- function itemCancelable(item = {}) {
302
- return ["preparing", "queued", "uploading"].includes(itemPhase(item));
303
- }
304
-
305
240
  function actionKey(item = {}, action = "") {
306
241
  return `${itemKey(item)}:${action}`;
307
242
  }
308
243
 
309
244
  function actionElement(element = null) {
310
- return element?.$el || element || null;
245
+ return element?.$el || null;
311
246
  }
312
247
 
313
248
  function setActionElement(item = {}, action = "", element = null) {
@@ -323,7 +258,7 @@ function focusItemAction(item = {}) {
323
258
  for (const action of ["cancel", "retry", "remove"]) {
324
259
  const target = actionElement(actionElements.get(actionKey(item, action)));
325
260
  if (target && !target.disabled) {
326
- target.focus?.();
261
+ target.focus();
327
262
  return true;
328
263
  }
329
264
  }
@@ -344,51 +279,33 @@ async function handleAction(action, item, index) {
344
279
  emit("focus-input");
345
280
  }
346
281
 
347
- function itemIcon(item = {}) {
348
- if (itemPhase(item) === "ready") {
349
- return mdiCheckCircleOutline;
350
- }
351
- if (itemPhase(item) === "failed") {
352
- return mdiAlertCircleOutline;
353
- }
354
- return mdiFileOutline;
355
- }
356
-
357
- function itemColor(item = {}) {
358
- if (itemPhase(item) === "ready") {
359
- return "success";
360
- }
361
- if (itemPhase(item) === "failed") {
362
- return "error";
363
- }
364
- return "primary";
365
- }
366
-
367
282
  function updateReducedMotionPreference() {
368
283
  reducedMotion.value = reducedMotionMediaQuery?.matches === true;
369
284
  }
370
285
 
371
- if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
286
+ if (hasWindowFunction("matchMedia")) {
372
287
  reducedMotionMediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
373
288
  updateReducedMotionPreference();
374
289
  }
290
+ // readable-check: allow method-probe - platform object: an older browser's media query has addListener() in place of addEventListener()
291
+ const mediaQueryDispatchesEvents = typeof reducedMotionMediaQuery?.addEventListener === "function";
375
292
 
376
293
  onMounted(() => {
377
294
  if (!reducedMotionMediaQuery) {
378
295
  return;
379
296
  }
380
- if (typeof reducedMotionMediaQuery.addEventListener === "function") {
297
+ if (mediaQueryDispatchesEvents) {
381
298
  reducedMotionMediaQuery.addEventListener("change", updateReducedMotionPreference);
382
299
  } else {
383
- reducedMotionMediaQuery.addListener?.(updateReducedMotionPreference);
300
+ reducedMotionMediaQuery.addListener(updateReducedMotionPreference);
384
301
  }
385
302
  });
386
303
 
387
304
  onBeforeUnmount(() => {
388
- if (typeof reducedMotionMediaQuery?.removeEventListener === "function") {
305
+ if (mediaQueryDispatchesEvents) {
389
306
  reducedMotionMediaQuery.removeEventListener("change", updateReducedMotionPreference);
390
307
  } else {
391
- reducedMotionMediaQuery?.removeListener?.(updateReducedMotionPreference);
308
+ reducedMotionMediaQuery?.removeListener(updateReducedMotionPreference);
392
309
  }
393
310
  reducedMotionMediaQuery = null;
394
311
  });
@@ -30,7 +30,7 @@ const { t } = useJskitText();
30
30
  defineProps({ state: { type: Object, required: true } });
31
31
  const emit = defineEmits(["submit", "stop"]);
32
32
  const sendButton = ref(null);
33
- defineExpose({ focus() { (sendButton.value?.$el || sendButton.value)?.focus?.(); } });
33
+ defineExpose({ focus() { sendButton.value?.$el.focus(); } });
34
34
  </script>
35
35
  <style scoped>
36
36
  .assistant-composer-actions, .assistant-composer-actions__tools, .assistant-composer-actions__delivery {
@@ -37,6 +37,7 @@
37
37
  </div>
38
38
  </div>
39
39
  </section>
40
+ <!-- readable-check: allow optional-call - optional operation: an adapter without loadMore, reload, checkDelivery or openLink offers no older history, reload, delivery check or link handling -->
40
41
  <AssistantTranscript
41
42
  v-bind="conversation" :preview-message="adapter.conversation.previewMessage || null" :working="working" class="assistant-conversation__transcript"
42
43
  @load-more="adapter.actions?.loadMore?.($event)" @reload="adapter.actions?.reload?.()"
@@ -96,9 +97,11 @@
96
97
  @submit="submit" @tab-to-submit="delivery?.focus()"
97
98
  @focus="adapter.suggestions?.focus()" @blur="adapter.suggestions?.blur()"
98
99
  @escape="dismissSuggestions"
99
- @dragenter="attachmentEvent('handleDragEnter', $event)" @dragover="attachmentEvent('handleDragOver', $event)"
100
- @dragleave="attachmentEvent('handleDragLeave', $event)" @drop="attachmentEvent('handleDrop', $event)"
101
- @paste="attachmentEvent('handlePaste', $event)"
100
+ @dragenter="attachmentsEnabled && adapter.attachments.handleDragEnter($event)"
101
+ @dragover="attachmentsEnabled && adapter.attachments.handleDragOver($event)"
102
+ @dragleave="attachmentsEnabled && adapter.attachments.handleDragLeave($event)"
103
+ @drop="attachmentsEnabled && adapter.attachments.handleDrop($event)"
104
+ @paste="attachmentsEnabled && adapter.attachments.handlePaste($event)"
102
105
  >
103
106
  <template v-if="attachmentsEnabled" #attachments>
104
107
  <input ref="fileInput" hidden multiple type="file" :accept="adapter.attachments.accept" :disabled="!adapter.attachments.canAddFiles" @change="attachSelectedFiles">
@@ -119,6 +122,7 @@
119
122
  </template>
120
123
  <template #footer>
121
124
  <AssistantComposerActions ref="delivery" :state="{ ...adapter.composer, pending, canSend }" @submit="submit" @stop="stop">
125
+ <!-- readable-check: allow optional-call - optional operation: model choices without a provider, model or variant selector, or without reload, ignore that change -->
122
126
  <AssistantModelControl
123
127
  v-if="adapter.models && adapter.models.enabled !== false" v-model="modelsOpen" v-bind="adapter.models"
124
128
  @select-provider="adapter.models.selectProvider?.($event)" @select-model="adapter.models.selectModel?.($event)"
@@ -163,11 +167,12 @@ import AssistantGoalControl from "./AssistantGoalControl.vue";
163
167
  import AssistantAttachmentQueue from "./AssistantAttachmentQueue.vue";
164
168
  import AssistantQuestionInputs from "./AssistantQuestionInputs.vue";
165
169
  import AssistantModelControl from "./AssistantModelControl.vue";
170
+ import { displayedConversation } from "./conversationDisplay.js";
166
171
  import { useJskitText } from "../i18n.js";
167
172
 
168
173
  const { t } = useJskitText();
169
174
  const props = defineProps({
170
- adapter: { type: Object, required: true },
175
+ adapter: { type: /** @type {import("vue").PropType<import("../types.js").AssistantConversationAdapter>} */ (Object), required: true },
171
176
  avatarSize: { type: String, default: "compact", validator: (value) => ["hidden", "compact", "standard", "large"].includes(value) },
172
177
  label: { type: String, default: undefined },
173
178
  configuration: { type: Object, default: () => ({}) },
@@ -247,52 +252,9 @@ const fileInput = ref(null);
247
252
  const modelsOpen = ref(false);
248
253
  const delivery = ref(null);
249
254
  const statusId = `assistant-status-${useId()}`;
250
- const conversation = computed(() => {
251
- const saved = props.adapter.conversation;
252
- if (!props.adapter.delivery && !saved.previewMessage && !saved.previewMessages?.length && !saved.interimReply) return saved;
253
- const { previewMessage, previewMessages, interimReply, ...display } = saved;
254
- let turns = props.adapter.delivery ? props.adapter.delivery.turns(saved.turns || []) : saved.turns || [];
255
- if (interimReply?.text && interimReply.turnId) {
256
- const assistant = { ...interimReply, messageId: interimReply.id, role: "assistant" };
257
- turns = turns.map(turn => turn.turnId !== interimReply.turnId ? turn : {
258
- ...turn, assistant,
259
- commentary: (turn.commentary || []).filter(message =>
260
- (message.outputId || message.messageId) !== (interimReply.outputId || interimReply.id)),
261
- ...(Array.isArray(turn.messages) ? { messages: turn.messages.filter(message =>
262
- message.role !== "assistant" &&
263
- (message.outputId || message.messageId) !== (interimReply.outputId || interimReply.id)).concat(assistant) } : {})
264
- });
265
- }
266
- // This list projects existing capture/pending owners; it never queues delivery.
267
- const previews = Array.isArray(previewMessages) ? previewMessages : previewMessage ? [previewMessage] : [];
268
- for (const preview of previews) {
269
- const messageId = String(preview?.messageId || preview?.id || "").trim();
270
- const localDelivery = messageId && props.adapter.delivery?.find?.(messageId);
271
- if ((!localDelivery || localDelivery.status === "failed") && preview?.actions?.send) {
272
- // Keep one unadmitted bubble and its real status until explicit Edit/discard.
273
- // A receipt:false user row is not acknowledgement of the pending words.
274
- turns = turns.map(turn => {
275
- const failed = turn.optimistic?.id === messageId && turn.optimistic.status === "failed";
276
- const unadmitted = turn.user?.receipt === false && String(turn.user.messageId || turn.user.id || "") === messageId;
277
- if (!failed && !unadmitted) return turn;
278
- return { ...turn, preview: true, previewMessage: preview, user: { ...turn.user, text: String(preview.text || "") } };
279
- });
280
- }
281
- const alreadyShown = messageId && (localDelivery || turns.some(turn => [
282
- turn.system, turn.user, turn.assistant, ...(turn.thinking || []), ...(turn.commentary || []), ...(turn.messages || [])
283
- ].some(message => String(message?.messageId || message?.id || "") === messageId &&
284
- (message.receipt !== false || turn.preview))));
285
- if (messageId && (preview?.text || preview?.actions?.editing) && !alreadyShown) {
286
- const user = { messageId, role: "user", text: String(preview.text || "") };
287
- turns = [...turns, { turnId: messageId, user, messages: [user], preview: true, previewMessage: preview,
288
- optimistic: { id: messageId, status: "pending" } }];
289
- }
290
- }
291
- // Transcription and interim replies are display only. The receipt watcher
292
- // receives canonical turns, never these presentation or delivery overlays.
293
- return { ...display, turns, welcomeMessage: turns.length ? "" : saved.welcomeMessage };
294
- });
255
+ const conversation = computed(() => displayedConversation(props.adapter));
295
256
  watch([() => props.adapter.delivery, () => props.adapter.conversation.turns], ([controller, turns]) => {
257
+ // readable-check: allow optional-collaborator - optional operation: an adapter without a delivery sends through its own actions
296
258
  controller?.reconcile(turns || []);
297
259
  }, { immediate: true });
298
260
  const pending = computed(() => Boolean(props.adapter.delivery?.state.sending || props.adapter.composer?.pending));
@@ -318,9 +280,11 @@ const canSend = computed(() => (
318
280
  async function resend(id) {
319
281
  if (props.adapter.actions?.resend) return props.adapter.actions.resend(id);
320
282
  const controller = props.adapter.delivery;
283
+ // readable-check: allow optional-collaborator - optional operation: an adapter without a delivery sends through its own actions
321
284
  const payload = controller?.find(id)?.payload;
322
285
  const attachments = props.adapter.attachments;
323
286
  try {
287
+ // readable-check: allow optional-collaborator - optional operation: an adapter without a delivery sends through its own actions
324
288
  const response = await controller?.resend(id, { queue: queueWhileSending.value });
325
289
  clearAcceptedAttachments(response, payload, attachments);
326
290
  return response;
@@ -343,9 +307,6 @@ function edit(id) {
343
307
  const draft = props.adapter.delivery?.edit(id, props.adapter.composer?.draft || "");
344
308
  if (draft != null) { props.adapter.actions.setDraft(draft); input.value?.focus(); }
345
309
  }
346
- function attachmentEvent(method, event) {
347
- if (attachmentsEnabled.value) return props.adapter.attachments[method]?.(event);
348
- }
349
310
  function attachSelectedFiles(event) {
350
311
  const files = Array.from(event.target.files || []);
351
312
  event.target.value = "";
@@ -397,6 +358,7 @@ function stop() {
397
358
  }
398
359
  function updateConfiguration(value) {
399
360
  if (props.configurationMode !== "editable" || pending.value) return;
361
+ // readable-check: allow optional-call - optional operation: an adapter without updateConfiguration keeps its configuration fixed
400
362
  return props.adapter.actions?.updateConfiguration?.(value);
401
363
  }
402
364
  defineExpose({ focus: () => input.value?.focus(), submit, stop });
@@ -8,16 +8,21 @@ const STATUS_KEYS = Object.freeze({
8
8
  usageLimited: "jskit.assistant.goal.usageLimited", budgetLimited: "jskit.assistant.goal.budgetLimited", complete: "jskit.assistant.goal.complete"
9
9
  });
10
10
 
11
- const props = defineProps({ state: { type: Object, default: null } });
11
+ const props = defineProps({
12
+ state: { type: /** @type {import("vue").PropType<import("../types.js").AssistantGoalState>} */ (Object), required: true }
13
+ });
12
14
  const open = ref(false);
13
15
  const objective = ref("");
14
16
  const tokenBudget = ref("");
15
17
  const now = ref(Date.now());
16
- const goal = computed(() => props.state?.loading || props.state?.available === false ? null : props.state?.goal);
18
+ const goal = computed(() => props.state.loading || props.state.available === false ? null : props.state.goal);
17
19
  const running = computed(() => goal.value?.status === "active");
18
- const working = computed(() => running.value && props.state?.working === true);
19
- const label = computed(() => props.state?.loading ? t("jskit.assistant.goal.loading") : props.state?.available === false ? t("jskit.assistant.goal.unavailable")
20
- : t(Object.hasOwn(STATUS_KEYS, goal.value?.status ?? "") ? STATUS_KEYS[goal.value.status] : "jskit.assistant.goal.set"));
20
+ const working = computed(() => running.value && props.state.working === true);
21
+ // A Start stays Starting… until the server's goal shows it, even while closed.
22
+ const starting = computed(() => props.state.action === "set");
23
+ const label = computed(() => props.state.loading ? t("jskit.assistant.goal.loading") : props.state.available === false ? t("jskit.assistant.goal.unavailable")
24
+ : starting.value ? t("jskit.assistant.goal.starting")
25
+ : t(Object.hasOwn(STATUS_KEYS, goal.value?.status ?? "") ? STATUS_KEYS[goal.value.status] : "jskit.assistant.goal.set"));
21
26
  const nativeElapsed = computed(() => Number.isFinite(goal.value?.elapsedSeconds));
22
27
  const createdAt = computed(() => typeof goal.value?.createdAt === "string" ? Date.parse(goal.value.createdAt) : NaN);
23
28
  const elapsedLabel = computed(() => nativeElapsed.value ? t("jskit.assistant.goal.runningTime") : t("jskit.assistant.goal.elapsedSinceStart"));
@@ -40,18 +45,19 @@ const elapsed = computed(() => {
40
45
  const minutes = Math.floor(total / 60);
41
46
  return minutes < 60 ? `${minutes}:${seconds}` : `${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, "0")}:${seconds}`;
42
47
  });
43
- const canSet = computed(() => !props.state?.loading && props.state?.available !== false && typeof props.state?.set === "function" && (!goal.value || goal.value.status === "complete"));
44
- const validBudget = computed(() => props.state?.tokenBudgetSupported === false || tokenBudget.value === "" || (Number.isSafeInteger(Number(tokenBudget.value)) && Number(tokenBudget.value) > 0));
48
+ // readable-check: allow method-probe - optional operation: a goal state without set() does not allow starting a goal, so Start stays disabled
49
+ const canSet = computed(() => !props.state.loading && props.state.available !== false && typeof props.state.set === "function" && (!goal.value || goal.value.status === "complete"));
50
+ const validBudget = computed(() => props.state.tokenBudgetSupported === false || tokenBudget.value === "" || (Number.isSafeInteger(Number(tokenBudget.value)) && Number(tokenBudget.value) > 0));
45
51
  let timer;
46
- onMounted(() => watch(() => [props.state?.enabled, clockRunning.value, goal.value?.elapsedSeconds, goal.value?.sampledAt, createdAt.value], () => {
52
+ onMounted(() => watch(() => [props.state.enabled, clockRunning.value, goal.value?.elapsedSeconds, goal.value?.sampledAt, createdAt.value], () => {
47
53
  clearInterval(timer);
48
54
  now.value = Date.now();
49
- if (props.state?.enabled !== false && clockRunning.value) {
55
+ if (props.state.enabled !== false && clockRunning.value) {
50
56
  timer = setInterval(() => { now.value = Date.now(); }, 1000);
51
57
  }
52
58
  }, { immediate: true, flush: "sync" }));
53
59
  onScopeDispose(() => clearInterval(timer));
54
- watch(() => props.state?.enabled, enabled => { if (enabled === false) open.value = false; });
60
+ watch(() => props.state.enabled, enabled => { if (enabled === false) open.value = false; });
55
61
  async function setGoal() {
56
62
  if (!canSet.value || props.state.pending || !objective.value.trim() || !validBudget.value) return;
57
63
  const result = await props.state.set({ objective: objective.value.trim(), ...(props.state.tokenBudgetSupported === false || tokenBudget.value === "" ? {} : { tokenBudget: Number(tokenBudget.value) }) });
@@ -64,7 +70,7 @@ async function setGoal() {
64
70
  <template #activator="{ props: menuProps }">
65
71
  <v-btn v-bind="menuProps" class="assistant-goal" size="small" variant="text" :aria-label="label" :title="[label, elapsedText, goal?.objective].filter(Boolean).join('\n')">
66
72
  <span v-if="goal" class="assistant-goal__light" :class="{ 'assistant-goal__light--running': running, 'assistant-goal__light--working': working, 'assistant-goal__light--paused': goal.status === 'paused' }" aria-hidden="true" />
67
- <span class="assistant-goal__label">{{ state.loading ? t('jskit.assistant.goal.loading') : state.available === false ? t('jskit.assistant.goal.unavailable') : goal ? t('jskit.assistant.goal.goal') : t('jskit.assistant.goal.set') }}</span>
73
+ <span class="assistant-goal__label">{{ state.loading ? t('jskit.assistant.goal.loading') : state.available === false ? t('jskit.assistant.goal.unavailable') : starting ? t('jskit.assistant.goal.starting') : goal ? t('jskit.assistant.goal.goal') : t('jskit.assistant.goal.set') }}</span>
68
74
  <span v-if="elapsed" class="assistant-goal__elapsed" aria-hidden="true">{{ elapsed }}</span>
69
75
  </v-btn>
70
76
  </template>
@@ -81,7 +87,7 @@ async function setGoal() {
81
87
  <form v-if="canSet" class="assistant-goal__form mt-2" @submit.prevent="setGoal">
82
88
  <v-textarea v-model="objective" :label="t('jskit.assistant.goal.objective')" rows="2" max-rows="4" maxlength="4000" auto-grow :disabled="state.pending" hide-details />
83
89
  <v-text-field v-if="state.tokenBudgetSupported !== false" v-model="tokenBudget" :label="t('jskit.assistant.goal.tokenBudget')" type="number" min="1" step="1" :disabled="state.pending" :error="!validBudget" hide-details />
84
- <v-btn type="submit" size="small" :disabled="state.pending || !objective.trim() || !validBudget">{{ state.action === 'set' ? t('jskit.assistant.goal.starting') : t('jskit.assistant.goal.start') }}</v-btn>
90
+ <v-btn type="submit" size="small" :disabled="state.pending || !objective.trim() || !validBudget">{{ starting ? t('jskit.assistant.goal.starting') : t('jskit.assistant.goal.start') }}</v-btn>
85
91
  </form>
86
92
  <p v-if="state.error" role="alert" class="text-error text-body-small mt-2">{{ state.error }}</p>
87
93
  <slot :goal="goal" />
@@ -77,7 +77,8 @@
77
77
  </template>
78
78
 
79
79
  <script setup>
80
- import { computed, onBeforeUnmount, onMounted, ref, useId, watch } from "vue";
80
+ import { computed, ref, useId } from "vue";
81
+ import { useTextareaAutoGrow } from "./useTextareaAutoGrow.js";
81
82
  const emit = defineEmits(["blur", "escape", "focus", "input-activity", "submit", "tab-to-submit", "update:modelValue", "paste"]);
82
83
  const props = defineProps({
83
84
  submitOnModifierEnter: { type: Boolean, default: false },
@@ -155,9 +156,6 @@ const props = defineProps({
155
156
 
156
157
  const textareaRef = ref(null);
157
158
  const textareaId = `assistant-prompt-${useId()}`;
158
- let resizeFrame = 0;
159
- let resizeObserver = null;
160
- let preserveHeightForNextModelValueChange = false;
161
159
  const combinedErrorMessages = computed(() => {
162
160
  return Array.isArray(props.errorMessages)
163
161
  ? props.errorMessages
@@ -177,55 +175,10 @@ const textareaAriaLabel = computed(() => (
177
175
  : undefined
178
176
  ));
179
177
 
180
- function resizeTextarea() {
181
- if (!props.autoGrow) {
182
- return;
183
- }
184
- const textarea = textareaRef.value;
185
- if (!textarea) {
186
- return;
187
- }
188
- if (!props.placeholderAffectsHeight && !textarea.value) {
189
- return;
190
- }
191
- const style = window.getComputedStyle(textarea);
192
- const minHeight = Number.parseFloat(style.minHeight) || 0;
193
- const maxHeight = Number.parseFloat(style.maxHeight) || Number.POSITIVE_INFINITY;
194
- textarea.style.height = "auto";
195
- const contentHeight = Math.max(textarea.scrollHeight, minHeight);
196
- const targetHeight = Math.min(contentHeight, maxHeight);
197
- textarea.style.height = `${targetHeight}px`;
198
- textarea.style.overflowY = contentHeight > targetHeight + 1 ? "auto" : "hidden";
199
- }
178
+ const { preserveHeightForNextModelValue, queueResizeTextarea, releasePreservedHeight } = useTextareaAutoGrow(props, textareaRef);
200
179
 
201
- function queueResizeTextarea() {
202
- if (!props.autoGrow || typeof window === "undefined") {
203
- return;
204
- }
205
- if (resizeFrame) return;
206
- resizeFrame = window.requestAnimationFrame(() => {
207
- resizeFrame = 0;
208
- resizeTextarea();
209
- });
210
- }
211
-
212
- function preserveHeightForNextModelValue() {
213
- const textarea = textareaRef.value;
214
- if (!props.autoGrow || !textarea) {
215
- return false;
216
- }
217
- if (resizeFrame) {
218
- window.cancelAnimationFrame(resizeFrame);
219
- resizeFrame = 0;
220
- }
221
- preserveHeightForNextModelValueChange = true;
222
- textarea.style.height = `${textarea.getBoundingClientRect().height}px`;
223
- textarea.style.overflowY = "auto";
224
- return true;
225
- }
226
-
227
- function handleTextareaInput(event = {}) {
228
- preserveHeightForNextModelValueChange = false;
180
+ function handleTextareaInput(event) {
181
+ releasePreservedHeight();
229
182
  emit("input-activity");
230
183
  emit("update:modelValue", String(event?.target?.value || ""));
231
184
  queueResizeTextarea();
@@ -289,47 +242,9 @@ function handleTextareaKeydown(event = {}) {
289
242
  }
290
243
 
291
244
  function focusTextarea(options = { preventScroll: true }) {
292
- textareaRef.value?.focus?.(options);
245
+ textareaRef.value?.focus(options);
293
246
  }
294
247
 
295
- onMounted(() => {
296
- queueResizeTextarea();
297
- let lastWidth = 0;
298
- resizeObserver = new ResizeObserver(([entry]) => {
299
- // Height changes are our own output. Only width changes need another layout.
300
- const width = entry.contentRect.width;
301
- if (width !== lastWidth) {
302
- lastWidth = width;
303
- queueResizeTextarea();
304
- }
305
- });
306
- resizeObserver.observe(textareaRef.value);
307
- });
308
-
309
- onBeforeUnmount(() => {
310
- resizeObserver?.disconnect();
311
- if (resizeFrame && typeof window !== "undefined") {
312
- window.cancelAnimationFrame(resizeFrame);
313
- resizeFrame = 0;
314
- }
315
- });
316
-
317
- watch(() => [
318
- props.autoGrow,
319
- props.modelValue,
320
- props.placeholder,
321
- props.placeholderAffectsHeight,
322
- props.rows,
323
- props.density
324
- ], (values, previousValues = []) => {
325
- const modelValueChanged = values[1] !== previousValues[1];
326
- if (preserveHeightForNextModelValueChange && modelValueChanged) {
327
- preserveHeightForNextModelValueChange = false;
328
- return;
329
- }
330
- queueResizeTextarea();
331
- }, { flush: "post" });
332
-
333
248
  defineExpose({ inputElement: textareaRef, focus: focusTextarea, preserveHeightForNextModelValue, queueResizeTextarea });
334
249
  </script>
335
250
  <style scoped>