@meistrari/chat-nuxt 3.11.0 → 3.12.0-rc.0

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 (38) hide show
  1. package/README.md +1 -1
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/assets/css/markstream.css +1 -1
  4. package/dist/runtime/embed/components/chat-embed-inner.vue +76 -36
  5. package/dist/runtime/embed/composables/use-agent-wake.d.ts +1 -2
  6. package/dist/runtime/embed/composables/use-agent-wake.js +19 -13
  7. package/dist/runtime/embed/feature-config.d.ts +2 -0
  8. package/dist/runtime/embed/feature-config.js +2 -1
  9. package/dist/runtime/embed/loading-messages.d.ts +1 -1
  10. package/dist/runtime/embed/loading-messages.js +1 -1
  11. package/dist/runtime/messages/components/chat-message-bubble.vue +4 -4
  12. package/dist/runtime/messages/components/chat-message-input.d.vue.ts +8 -2
  13. package/dist/runtime/messages/components/chat-message-input.vue +51 -2
  14. package/dist/runtime/messages/components/chat-message-input.vue.d.ts +8 -2
  15. package/dist/runtime/messages/components/chat-message-list.vue +3 -5
  16. package/dist/runtime/messages/components/reasoning/chat-reasoning-steps.vue +1 -1
  17. package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue +14 -66
  18. package/dist/runtime/messages/composables/chat-message-terminal.js +3 -3
  19. package/dist/runtime/messages/composables/chat.d.ts +17 -3
  20. package/dist/runtime/messages/composables/chat.js +246 -23
  21. package/dist/runtime/messages/composables/outbound-queue.d.ts +23 -0
  22. package/dist/runtime/messages/composables/outbound-queue.js +28 -0
  23. package/dist/runtime/messages/composables/use-direct-stream.d.ts +7 -1
  24. package/dist/runtime/messages/composables/use-direct-stream.js +30 -7
  25. package/dist/runtime/server/utils/chat-api-proxy.js +3 -1
  26. package/dist/runtime/shared/chat-api-error.d.ts +7 -0
  27. package/dist/runtime/shared/chat-api-error.js +24 -4
  28. package/dist/runtime/types/schemas/chat/chat-config.d.ts +1 -0
  29. package/dist/runtime/types/schemas/chat/chat-config.js +1 -0
  30. package/dist/runtime/types/schemas/chat/messages.d.ts +14 -0
  31. package/dist/runtime/types/schemas/chat/messages.js +6 -1
  32. package/dist/runtime/types/schemas/chat/ui.d.ts +2 -2
  33. package/dist/runtime/types/schemas/chat/ui.js +2 -2
  34. package/dist/runtime/types/schemas/index.d.ts +1 -0
  35. package/dist/runtime/types/schemas/index.js +1 -0
  36. package/dist/runtime/types/schemas/integrations/tela-agent-api.d.ts +46 -0
  37. package/dist/runtime/types/schemas/integrations/tela-agent-api.js +26 -0
  38. package/package.json +2 -2
package/README.md CHANGED
@@ -101,7 +101,7 @@ Installing `@meistrari/chat-nuxt` installs the runtime packages used by the laye
101
101
 
102
102
  | Package | Version | Purpose |
103
103
  |---------|---------|---------|
104
- | `@meistrari/auth-nuxt` | `3.5.0` | Authentication (required) |
104
+ | `@meistrari/auth-nuxt` | `3.31.0` | Authentication (required) |
105
105
  | `@meistrari/logger` | `^2.1.3` | Structured logging for the server proxy routes |
106
106
  | `@meistrari/tela-build` | `^1.30.0` | UI component library (Nuxt layer) |
107
107
  | `@sentry/nuxt` | `^10.0.0` | Client-side error capture used by chat composables |
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "3.11.0",
4
+ "version": "3.12.0-rc.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -1 +1 @@
1
- .markstream-vue{color:#171717;font-family:Inter,"Inter Fallback: Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:-.15px;line-height:24px;--list-item-marker:#a3a3a3}.markstream-vue code,.markstream-vue kbd,.markstream-vue pre,.markstream-vue samp{font-family:Geist Mono Variable,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:13px;font-variation-settings:normal;line-height:1.5}.markstream-vue ol,.markstream-vue ul{margin:8px 0;padding-left:1.5em}.markstream-vue .hr-node{border-color:#e5e5e5;border-top-width:.5px;margin-bottom:24px;margin-top:24px}.markstream-vue .strong-node{font-weight:580}.markstream-vue .list-node.list-node{margin-bottom:8px;margin-left:4px;margin-top:8px;padding-left:1em}.markstream-vue ul{list-style:disc;list-style-position:outside}.markstream-vue ol{list-style:decimal;list-style-position:outside}.markstream-vue li{margin:4px 0}.markstream-vue .paragraph-node{margin:0}.markstream-vue .my-8{margin-bottom:8px;margin-top:8px}.markstream-vue .heading-node.heading-1,.markstream-vue h1{font-size:24px;font-weight:540;letter-spacing:-.8px;line-height:28px;margin:24px 0 16px}.markstream-vue .heading-node.heading-2,.markstream-vue h2{font-size:20px;font-weight:540;letter-spacing:-.5px;line-height:24px;margin:20px 0 12px}.markstream-vue .heading-node.heading-3,.markstream-vue h3{font-size:20px;font-weight:540;letter-spacing:-.5px;line-height:24px;margin:16px 0 8px}.markstream-vue .heading-node.heading-4,.markstream-vue h4{font-size:16px;font-weight:540;letter-spacing:-.2px;line-height:20px;margin:12px 0 6px}.markstream-vue .heading-node.heading-5,.markstream-vue h5{font-size:14px;font-weight:540;letter-spacing:-.15px;line-height:16px;margin:8px 0 4px}.markstream-vue .heading-node.heading-6,.markstream-vue h6{font-size:12px;font-weight:540;letter-spacing:-.1px;line-height:14px;margin:8px 0 4px}.markstream-vue [class*=tooltip],[class*=tooltip-element]{background-color:var(--neutral-900,#1a1a1a)!important;border-radius:4px;color:#fff!important;font-size:12px;padding:4px 8px}.mention-tag{align-items:center;border-radius:9999px;display:inline-flex;font-size:.875em;font-weight:500;gap:4px;padding:2px 8px;vertical-align:middle;white-space:nowrap}.mention-tag-file{background-color:rgba(127,189,247,.12);color:#7fbdf7}.mention-tag-skill{background-color:rgba(155,111,222,.12);color:#9b6fde}.mention-tag-canvas{background-color:rgba(234,161,65,.12);color:#d4882e}.mention-tag>span[class*=i-ph]{flex-shrink:0}
1
+ .markstream-vue{color:#171717;font-family:Inter,"Inter Fallback: Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:-.15px;line-height:24px;--list-item-marker:#a3a3a3;--list-item-counter-marker:#262626}.markstream-vue code,.markstream-vue kbd,.markstream-vue pre,.markstream-vue samp{font-family:Geist Mono Variable,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:13px;font-variation-settings:normal;line-height:1.5}.markstream-vue ol,.markstream-vue ul{margin:8px 0;padding-left:1.5em}.markstream-vue .hr-node{border-color:#e5e5e5;border-top-width:.5px;margin-bottom:24px;margin-top:24px}.markstream-vue .strong-node{font-weight:580}.markstream-vue .list-node.list-node{margin-bottom:8px;margin-left:4px;margin-top:8px;padding-left:1em}.markstream-vue ul{list-style:disc;list-style-position:outside}.markstream-vue ol{list-style:decimal;list-style-position:outside}.markstream-vue li{margin:4px 0}.markstream-vue .paragraph-node{margin:0}.markstream-vue .my-8{margin-bottom:8px;margin-top:8px}.markstream-vue .heading-node.heading-1,.markstream-vue h1{font-size:24px;font-weight:540;letter-spacing:-.8px;line-height:28px;margin:24px 0 16px}.markstream-vue .heading-node.heading-2,.markstream-vue h2{font-size:20px;font-weight:540;letter-spacing:-.5px;line-height:24px;margin:20px 0 12px}.markstream-vue .heading-node.heading-3,.markstream-vue h3{font-size:20px;font-weight:540;letter-spacing:-.5px;line-height:24px;margin:16px 0 8px}.markstream-vue .heading-node.heading-4,.markstream-vue h4{font-size:16px;font-weight:540;letter-spacing:-.2px;line-height:20px;margin:12px 0 6px}.markstream-vue .heading-node.heading-5,.markstream-vue h5{font-size:14px;font-weight:540;letter-spacing:-.15px;line-height:16px;margin:8px 0 4px}.markstream-vue .heading-node.heading-6,.markstream-vue h6{font-size:12px;font-weight:540;letter-spacing:-.1px;line-height:14px;margin:8px 0 4px}.markstream-vue [class*=tooltip],[class*=tooltip-element]{background-color:var(--neutral-900,#1a1a1a)!important;border-radius:4px;color:#fff!important;font-size:12px;padding:4px 8px}.mention-tag{align-items:center;border-radius:9999px;display:inline-flex;font-size:.875em;font-weight:500;gap:4px;padding:2px 8px;vertical-align:middle;white-space:nowrap}.mention-tag-file{background-color:rgba(127,189,247,.12);color:#7fbdf7}.mention-tag-skill{background-color:rgba(155,111,222,.12);color:#9b6fde}.mention-tag-canvas{background-color:rgba(234,161,65,.12);color:#d4882e}.mention-tag>span[class*=i-ph]{flex-shrink:0}
@@ -143,6 +143,9 @@ function applySuggestion(prompt) {
143
143
  const breakpoints = useBreakpoints({ mobile: CHAT_MOBILE_BREAKPOINT });
144
144
  const isMobile = breakpoints.smaller("mobile");
145
145
  const drawerOpen = ref(false);
146
+ function restoreQueuedSend(send) {
147
+ messageInputRef.value?.restoreDraft(send);
148
+ }
146
149
  const { isDragging, setupDropZone } = useFileDrop({
147
150
  onDrop: (files) => {
148
151
  messageInputRef.value?.addFiles(files);
@@ -155,7 +158,10 @@ const {
155
158
  loading,
156
159
  generatingTitle,
157
160
  animatedTitle,
158
- hasPendingMessage,
161
+ composerLocked,
162
+ outboundOccupied,
163
+ sessionReady,
164
+ agentSessionRewakeable,
159
165
  streamingActiveMessageId,
160
166
  preparingSteps,
161
167
  loadConversation,
@@ -164,12 +170,18 @@ const {
164
170
  setConversationModel,
165
171
  pendingModelSelection,
166
172
  sendMessage,
173
+ flushOutboundQueue,
174
+ failOutboundQueue,
167
175
  retryMessage,
168
176
  cancelConversation,
169
177
  startPolling,
170
178
  clearConversation,
171
179
  error: chatError
172
- } = useChat();
180
+ } = useChat({
181
+ outboundQueueEnabled: computed(() => resolvedFeatures.value.chatOutboundQueue),
182
+ onQueuedSendCancelled: restoreQueuedSend,
183
+ onQueuedSendFailed: restoreQueuedSend
184
+ });
173
185
  const agentWake = useAgentWake();
174
186
  const modelUpdateInFlight = ref(null);
175
187
  watch(chatError, (message) => {
@@ -186,9 +198,11 @@ const selectedModel = computed({
186
198
  if (conversation && conversation.model !== value) {
187
199
  const previousModel = conversation.model;
188
200
  setConversationModel(value);
189
- const update = updateConversationModel(conversation.id, value).then((updated) => {
201
+ const update = updateConversationModel(conversation.id, value).then(async (updated) => {
190
202
  if (!updated && currentConversation.value?.id === conversation.id && currentConversation.value.model === value) {
191
203
  setConversationModel(previousModel);
204
+ } else if (updated && value) {
205
+ await agentWake.wake(conversation.threadSessionId ?? void 0, value);
192
206
  }
193
207
  return updated;
194
208
  });
@@ -199,11 +213,6 @@ const selectedModel = computed({
199
213
  }
200
214
  }
201
215
  });
202
- watch(selectedModel, (model, previousModel) => {
203
- if (!isReady.value || model === previousModel || currentConversation.value?.threadSessionId)
204
- return;
205
- void agentWake.wake(model);
206
- });
207
216
  const filesCount = computed(() => total.value);
208
217
  const hasMessages = computed(() => messages.value.length > 0);
209
218
  const mobilePreviewFile = computed(() => {
@@ -244,8 +253,8 @@ function updateConversationId(nextConversationId) {
244
253
  async function ensureConversationLoaded(conversationId) {
245
254
  if (currentConversation.value?.id === conversationId) {
246
255
  void agentWake.wake(
247
- currentConversation.value.model,
248
- currentConversation.value.threadSessionId ?? void 0
256
+ currentConversation.value.threadSessionId ?? void 0,
257
+ selectedModel.value
249
258
  );
250
259
  startPolling();
251
260
  return;
@@ -265,8 +274,8 @@ async function ensureConversationLoaded(conversationId) {
265
274
  return;
266
275
  }
267
276
  void agentWake.wake(
268
- currentConversation.value.model,
269
- currentConversation.value.threadSessionId ?? void 0
277
+ currentConversation.value.threadSessionId ?? void 0,
278
+ selectedModel.value
270
279
  );
271
280
  startPolling();
272
281
  } finally {
@@ -277,7 +286,7 @@ async function ensureConversationLoaded(conversationId) {
277
286
  }
278
287
  function resetConversation(shouldEmit = true) {
279
288
  clearConversation();
280
- void agentWake.wake(selectedModel.value);
289
+ void agentWake.wake(void 0, selectedModel.value);
281
290
  activeTab.value = "chat";
282
291
  closeFilesPanel();
283
292
  closePreview();
@@ -460,29 +469,60 @@ async function handleSend(content, files, agentInputs) {
460
469
  const conversation = currentConversation.value;
461
470
  if (!conversation)
462
471
  throw new Error("Erro ao preparar conversa");
463
- if (conversation.threadSessionId) {
464
- await agentWake.wake(
465
- conversation.model,
466
- conversation.threadSessionId
467
- );
468
- } else {
472
+ if (conversation.threadSessionId && resolvedFeatures.value.chatOutboundQueue && sessionReady.value === false) {
473
+ if (!agentSessionRewakeable.value)
474
+ return { defer: true };
475
+ const wake = await agentWake.wake(conversation.threadSessionId, selectedModel.value);
476
+ if (wake.status !== "ready")
477
+ throw new Error("N\xE3o foi poss\xEDvel acordar o agente. Tente novamente.");
478
+ return {};
479
+ }
480
+ if (!conversation.threadSessionId) {
469
481
  await agentWake.waitUntilSettled(signal);
482
+ if (agentWake.state.value === "idle")
483
+ await agentWake.wake(void 0, selectedModel.value);
484
+ else if (agentWake.state.value === "ready")
485
+ await agentWake.wake(void 0, selectedModel.value);
470
486
  }
471
487
  claimedWakeSessionId = claimableWakeSessionId();
488
+ if (!conversation.threadSessionId && !claimedWakeSessionId)
489
+ throw new Error("N\xE3o foi poss\xEDvel acordar o agente. Tente novamente.");
472
490
  return claimedWakeSessionId ? { wakeSessionId: claimedWakeSessionId } : {};
473
491
  });
474
492
  if (sent && claimedWakeSessionId)
475
493
  agentWake.consume();
494
+ if (!sent)
495
+ restoreQueuedSend({ content, files, agentInputs: resolvedAgentInputs });
496
+ }
497
+ watch(
498
+ [outboundOccupied, agentWake.state],
499
+ async ([occupied, wakeState]) => {
500
+ if (!occupied)
501
+ return;
502
+ if (wakeState === "ready") {
503
+ const wakeSessionId = claimableWakeSessionId();
504
+ const sent = await flushOutboundQueue(wakeSessionId);
505
+ if (sent && wakeSessionId)
506
+ agentWake.consume();
507
+ } else if (wakeState === "unavailable" && (agentSessionRewakeable.value || sessionReady.value === null)) {
508
+ failOutboundQueue("N\xE3o foi poss\xEDvel acordar o agente. Tente novamente.");
509
+ }
510
+ }
511
+ );
512
+ watch(
513
+ [outboundOccupied, agentSessionRewakeable],
514
+ ([occupied, rewakeable]) => {
515
+ const sessionId = currentConversation.value?.threadSessionId;
516
+ if (occupied && rewakeable && sessionId && agentWake.state.value !== "waking")
517
+ void agentWake.wake(sessionId, selectedModel.value);
518
+ }
519
+ );
520
+ async function handleCancel() {
521
+ await cancelConversation();
476
522
  }
477
523
  async function handleRetry(messageId) {
478
- if (currentConversation.value?.threadSessionId) {
479
- await agentWake.wake(
480
- currentConversation.value.model,
481
- currentConversation.value.threadSessionId
482
- );
483
- } else {
524
+ if (!currentConversation.value?.threadSessionId)
484
525
  await agentWake.waitUntilSettled();
485
- }
486
526
  await retryMessage(messageId, claimableWakeSessionId());
487
527
  }
488
528
  async function handleNewConversation(groupId) {
@@ -770,7 +810,7 @@ async function handleDelete() {
770
810
  :files-count="filesCount"
771
811
  :generating-title="generatingTitle"
772
812
  :has-messages="hasMessages"
773
- :has-pending-message="hasPendingMessage"
813
+ :has-pending-message="composerLocked"
774
814
  :hide-settings="hideSettings"
775
815
  :hide-sidebar="hideSidebar"
776
816
  :is-editing="isEditing"
@@ -789,7 +829,7 @@ async function handleDelete() {
789
829
  :workspace-logo="workspaceLogo"
790
830
  :workspace-name="workspaceName"
791
831
  :workspace-settings="effectiveWorkspaceSettings"
792
- @cancel="cancelConversation"
832
+ @cancel="handleCancel"
793
833
  @cancel-editing="cancelEditing"
794
834
  @copy-link="handleShare"
795
835
  @debug="showDebugModal = true"
@@ -848,7 +888,7 @@ async function handleDelete() {
848
888
  <ChatMessageInput
849
889
  ref="messageInputRef"
850
890
  v-model:selected-model="selectedModel"
851
- :loading="hasPendingMessage"
891
+ :loading="composerLocked"
852
892
  :creator-name="currentUser?.name ?? void 0"
853
893
  :creator-email="
854
894
  currentConversation?.createdBy ?? currentUser?.email ?? void 0
@@ -864,7 +904,7 @@ async function handleDelete() {
864
904
  :show-cancel-button="resolvedFeatures.showCancelButton"
865
905
  centered
866
906
  @send="handleSend"
867
- @cancel="cancelConversation"
907
+ @cancel="handleCancel"
868
908
  />
869
909
  </template>
870
910
  </TelaChatEmptyState>
@@ -881,7 +921,7 @@ async function handleDelete() {
881
921
  <ChatMessageInput
882
922
  ref="messageInputRef"
883
923
  v-model:selected-model="selectedModel"
884
- :loading="hasPendingMessage"
924
+ :loading="composerLocked"
885
925
  :creator-name="currentUser?.name ?? void 0"
886
926
  :creator-email="
887
927
  currentConversation?.createdBy ?? currentUser?.email ?? void 0
@@ -897,7 +937,7 @@ async function handleDelete() {
897
937
  :show-cancel-button="resolvedFeatures.showCancelButton"
898
938
  has-disclaimer
899
939
  @send="handleSend"
900
- @cancel="cancelConversation"
940
+ @cancel="handleCancel"
901
941
  />
902
942
  </ChatMobileShell>
903
943
 
@@ -1045,7 +1085,7 @@ async function handleDelete() {
1045
1085
  <ChatMessageInput
1046
1086
  ref="messageInputRef"
1047
1087
  v-model:selected-model="selectedModel"
1048
- :loading="hasPendingMessage"
1088
+ :loading="composerLocked"
1049
1089
  :creator-name="currentUser?.name ?? void 0"
1050
1090
  :creator-email="
1051
1091
  currentConversation?.createdBy ?? currentUser?.email ?? void 0
@@ -1061,7 +1101,7 @@ async function handleDelete() {
1061
1101
  :show-cancel-button="resolvedFeatures.showCancelButton"
1062
1102
  centered
1063
1103
  @send="handleSend"
1064
- @cancel="cancelConversation"
1104
+ @cancel="handleCancel"
1065
1105
  />
1066
1106
  </template>
1067
1107
  </TelaChatEmptyState>
@@ -1081,7 +1121,7 @@ async function handleDelete() {
1081
1121
  <ChatMessageInput
1082
1122
  ref="messageInputRef"
1083
1123
  v-model:selected-model="selectedModel"
1084
- :loading="hasPendingMessage"
1124
+ :loading="composerLocked"
1085
1125
  :creator-name="currentUser?.name ?? void 0"
1086
1126
  :creator-email="
1087
1127
  currentConversation?.createdBy ?? currentUser?.email ?? void 0
@@ -1097,7 +1137,7 @@ async function handleDelete() {
1097
1137
  :show-cancel-button="resolvedFeatures.showCancelButton"
1098
1138
  has-disclaimer
1099
1139
  @send="handleSend"
1100
- @cancel="cancelConversation"
1140
+ @cancel="handleCancel"
1101
1141
  />
1102
1142
  </TelaChatBody>
1103
1143
 
@@ -2,7 +2,6 @@ import type { TelaAgentWakeResponse } from '../../types/schemas/integrations/tel
2
2
  export type AgentWakeSnapshot = {
3
3
  state: TelaAgentWakeResponse['status'] | 'idle';
4
4
  sessionId?: string;
5
- model?: string;
6
5
  };
7
6
  export declare function pollAgentWake(options: {
8
7
  request: () => Promise<TelaAgentWakeResponse>;
@@ -16,7 +15,7 @@ export declare function pollAgentWake(options: {
16
15
  export declare function useAgentWake(): {
17
16
  state: any;
18
17
  sessionId: any;
19
- wake: (model?: string | null, requestedSessionId?: string) => Promise<TelaAgentWakeResponse>;
18
+ wake: (requestedSessionId?: string, model?: string | null) => Promise<TelaAgentWakeResponse>;
20
19
  skip: () => void;
21
20
  consume: () => void;
22
21
  waitUntilSettled: (signal?: AbortSignal) => Promise<AgentWakeSnapshot>;
@@ -21,21 +21,30 @@ export async function pollAgentWake(options) {
21
21
  else
22
22
  signal?.addEventListener("abort", onAbort, { once: true });
23
23
  }));
24
- try {
25
- while (!options.signal?.aborted && now() <= deadline) {
24
+ let unavailableAttempts = 0;
25
+ while (!options.signal?.aborted && now() <= deadline) {
26
+ try {
26
27
  const response = await options.request();
27
28
  options.onResponse?.(response);
29
+ if (response.status === "unavailable" && unavailableAttempts < 2) {
30
+ unavailableAttempts += 1;
31
+ await wait(Math.min(500 * 2 ** (unavailableAttempts - 1), Math.max(0, deadline - now())), options.signal);
32
+ continue;
33
+ }
28
34
  if (response.status !== "waking")
29
35
  return response;
30
36
  const remaining = deadline - now();
31
37
  if (remaining <= 0)
32
38
  break;
33
39
  await wait(Math.min(options.pollIntervalMs ?? WAKE_POLL_INTERVAL_MS, remaining), options.signal);
40
+ } catch (error) {
41
+ if (error instanceof DOMException && error.name === "AbortError")
42
+ throw error;
43
+ if (unavailableAttempts >= 2)
44
+ return { status: "unavailable" };
45
+ unavailableAttempts += 1;
46
+ await wait(Math.min(500 * 2 ** (unavailableAttempts - 1), Math.max(0, deadline - now())), options.signal);
34
47
  }
35
- } catch (error) {
36
- if (error instanceof DOMException && error.name === "AbortError")
37
- throw error;
38
- return { status: "unavailable" };
39
48
  }
40
49
  return { status: "unavailable" };
41
50
  }
@@ -67,7 +76,7 @@ export function useAgentWake() {
67
76
  activeWake = null;
68
77
  setSnapshot({ state: "idle" });
69
78
  }
70
- async function wake(model, requestedSessionId) {
79
+ async function wake(requestedSessionId, model) {
71
80
  generation++;
72
81
  const currentGeneration = generation;
73
82
  abortController?.abort();
@@ -76,8 +85,7 @@ export function useAgentWake() {
76
85
  let wakeSessionId = requestedSessionId ?? snapshot.value.sessionId;
77
86
  setSnapshot({
78
87
  state: "waking",
79
- ...wakeSessionId ? { sessionId: wakeSessionId } : {},
80
- ...model ? { model } : {}
88
+ ...wakeSessionId ? { sessionId: wakeSessionId } : {}
81
89
  });
82
90
  if (import.meta.server) {
83
91
  const unavailable = { status: "unavailable" };
@@ -105,16 +113,14 @@ export function useAgentWake() {
105
113
  wakeSessionId = response.sessionId ?? wakeSessionId;
106
114
  setSnapshot({
107
115
  state: response.status,
108
- ...wakeSessionId ? { sessionId: wakeSessionId } : {},
109
- ...model ? { model } : {}
116
+ ...wakeSessionId ? { sessionId: wakeSessionId } : {}
110
117
  });
111
118
  }
112
119
  });
113
120
  if (currentGeneration === generation) {
114
121
  setSnapshot({
115
122
  state: result.status,
116
- ...result.sessionId ? { sessionId: result.sessionId } : {},
117
- ...model ? { model } : {}
123
+ ...result.sessionId ? { sessionId: result.sessionId } : {}
118
124
  });
119
125
  }
120
126
  return result;
@@ -8,11 +8,13 @@ export type ChatFeatureConfig = {
8
8
  showCancelButton?: boolean;
9
9
  /** Time to first token shown in assistant message metadata. Defaults to true. */
10
10
  showTtft?: boolean;
11
+ chatOutboundQueue?: boolean;
11
12
  };
12
13
  export type ResolvedChatFeatureConfig = {
13
14
  showUsageTab: boolean;
14
15
  showDebugOption: boolean;
15
16
  showCancelButton: boolean;
16
17
  showTtft: boolean;
18
+ chatOutboundQueue: boolean;
17
19
  };
18
20
  export declare function resolveChatFeatures(features?: Partial<ChatFeatureConfig>): ResolvedChatFeatureConfig;
@@ -3,6 +3,7 @@ export function resolveChatFeatures(features) {
3
3
  showUsageTab: features?.showUsageTab ?? false,
4
4
  showDebugOption: features?.showDebugOption ?? false,
5
5
  showCancelButton: features?.showCancelButton ?? false,
6
- showTtft: features?.showTtft ?? true
6
+ showTtft: features?.showTtft ?? true,
7
+ chatOutboundQueue: features?.chatOutboundQueue ?? false
7
8
  };
8
9
  }
@@ -5,7 +5,7 @@ export declare const DEFAULT_CHAT_LOADING_MESSAGES: readonly ["Analisando sua pe
5
5
  * than as a loading placeholder. The user message is already delivered at this
6
6
  * point, so the wait belongs to the answer.
7
7
  */
8
- export declare const CHAT_PREPARING_STEPS: readonly ["Preparando o ambiente de trabalho", "Analisando a mensagem"];
8
+ export declare const CHAT_PREPARING_STEPS: readonly ["Preparando a resposta", "Analisando a mensagem"];
9
9
  /**
10
10
  * The first delay is measured from the send, every later one from the step
11
11
  * before it — so the sequence paces itself instead of racing a shared clock.
@@ -13,7 +13,7 @@ export const DEFAULT_CHAT_LOADING_MESSAGES = [
13
13
  "Estruturando resposta"
14
14
  ];
15
15
  export const CHAT_PREPARING_STEPS = [
16
- "Preparando o ambiente de trabalho",
16
+ "Preparando a resposta",
17
17
  "Analisando a mensagem"
18
18
  ];
19
19
  export const CHAT_PREPARING_STEP_DELAYS_MS = [2500, 4e3];
@@ -111,13 +111,13 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
111
111
  <template>
112
112
  <TelaChatMessage
113
113
  :from="message.role === 'user' ? 'user' : 'assistant'"
114
- :align="isOwnMessage ? 'end' : 'start'"
114
+ :align="message.role === 'user' ? 'end' : 'start'"
115
115
  >
116
116
  <template v-if="message.role === 'user'">
117
117
  <div
118
118
  max-w-prose
119
119
  flex="~ col"
120
- :class="isOwnMessage ? 'items-end' : 'items-start'"
120
+ items-end
121
121
  >
122
122
  <div
123
123
  v-if="!isOwnMessage"
@@ -138,7 +138,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
138
138
 
139
139
  <TelaChatMessageContent
140
140
  variant="contained"
141
- :align="isOwnMessage ? 'end' : 'start'"
141
+ align="end"
142
142
  :class="isPending && 'op-80'"
143
143
  >
144
144
  <div v-if="hasFiles" flex="~ col" gap-4px mb-8px>
@@ -171,7 +171,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
171
171
  mt-8px
172
172
  body-10-regular
173
173
  text-tertiary
174
- :class="isOwnMessage ? 'self-end' : 'self-start'"
174
+ self-end
175
175
  >
176
176
  {{ formatTime(message.createdAt) }}
177
177
  </div>
@@ -1,8 +1,8 @@
1
1
  import type { DeepReadonly } from 'vue';
2
2
  import type { Message } from '../../types/schemas/chat/conversations.js';
3
- import type { ConversationFile } from '../../types/schemas/chat/files.js';
3
+ import type { ConversationFile, FileUploadInput } from '../../types/schemas/chat/files.js';
4
4
  import type { WorkspaceSettingsResponse as WorkspaceSettings } from '../../types/schemas/chat/workspace-settings.js';
5
- import type { TelaAgentContextFile, TelaAgentInputVariable, TelaAgentSkill } from '../../types/schemas/integrations/tela-agent-api.js';
5
+ import type { TelaAgentContextFile, TelaAgentExecutionInput, TelaAgentInputVariable, TelaAgentSkill } from '../../types/schemas/integrations/tela-agent-api.js';
6
6
  type __VLS_Props = {
7
7
  disabled?: boolean;
8
8
  loading?: boolean;
@@ -21,6 +21,11 @@ type __VLS_Props = {
21
21
  showCancelButton?: boolean;
22
22
  };
23
23
  declare function addFiles(files: File[], inputName?: string): void;
24
+ declare function restoreDraft(draft: {
25
+ content: string;
26
+ files?: FileUploadInput[];
27
+ agentInputs?: TelaAgentExecutionInput[];
28
+ }): void;
24
29
  declare function setDraft(text: string): void;
25
30
  type __VLS_ModelProps = {
26
31
  'selectedModel'?: string | null;
@@ -28,6 +33,7 @@ type __VLS_ModelProps = {
28
33
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
29
34
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
30
35
  addFiles: typeof addFiles;
36
+ restoreDraft: typeof restoreDraft;
31
37
  setDraft: typeof setDraft;
32
38
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
33
39
  "update:selectedModel": (value: string | null) => any;
@@ -196,6 +196,15 @@ function handleFileSelect(event) {
196
196
  function createPendingFile(file, inputName) {
197
197
  return {
198
198
  id: `${file.name}-${file.lastModified}-${file.size}-${Math.random().toString(36).slice(2)}`,
199
+ source: "browser",
200
+ file,
201
+ inputName
202
+ };
203
+ }
204
+ function createUploadedPendingFile(file, inputName) {
205
+ return {
206
+ id: `${file.vaultReference ?? file.url}-${Math.random().toString(36).slice(2)}`,
207
+ source: "uploaded",
199
208
  file,
200
209
  inputName
201
210
  };
@@ -231,6 +240,22 @@ function handleVariableDrop(event, inputName) {
231
240
  function getVaultRef(file) {
232
241
  return file.vaultReference?.trim() || (file.url.startsWith("vault://") ? file.url : null);
233
242
  }
243
+ function getPendingFileMimeType(file) {
244
+ return file.source === "browser" ? file.file.type : file.file.mimeType;
245
+ }
246
+ async function resolvePendingFiles(files) {
247
+ const browserFiles = files.filter((file) => file.source === "browser");
248
+ const uploadedBrowserFiles = browserFiles.length > 0 ? await uploadFiles(browserFiles.map((file) => file.file)) : [];
249
+ let uploadedIndex = 0;
250
+ return files.map((file) => {
251
+ if (file.source === "uploaded")
252
+ return file.file;
253
+ const uploaded = uploadedBrowserFiles[uploadedIndex++];
254
+ if (!uploaded)
255
+ throw new Error("Falha ao preparar anexo");
256
+ return uploaded;
257
+ });
258
+ }
234
259
  function buildAgentInputs(files, queuedFiles) {
235
260
  if (!hasAgentInputs.value)
236
261
  return [];
@@ -266,7 +291,7 @@ async function handleSend() {
266
291
  const queuedFiles = [...pendingFiles.value];
267
292
  if (queuedFiles.length > 0) {
268
293
  try {
269
- files = await uploadFiles(queuedFiles.map((item) => item.file));
294
+ files = await resolvePendingFiles(queuedFiles);
270
295
  } catch {
271
296
  return;
272
297
  }
@@ -470,6 +495,29 @@ function addFiles(files, inputName) {
470
495
  });
471
496
  queueFiles(validFiles, inputName);
472
497
  }
498
+ function restoreDraft(draft) {
499
+ setDraft(draft.content);
500
+ const textInputs = (draft.agentInputs ?? []).filter((input) => input.type === "text");
501
+ agentInputTexts.value = {
502
+ ...agentInputTexts.value,
503
+ ...Object.fromEntries(textInputs.map((input) => [input.name, input.content]))
504
+ };
505
+ const availableSlots = MAX_FILES - pendingFiles.value.length;
506
+ if (availableSlots <= 0 || !draft.files?.length)
507
+ return;
508
+ const fileInputs = (draft.agentInputs ?? []).filter((input) => input.type === "file");
509
+ const claimedInputs = /* @__PURE__ */ new Set();
510
+ const restoredFiles = draft.files.slice(0, availableSlots).map((file) => {
511
+ const vaultRef = getVaultRef(file);
512
+ const inputIndex = fileInputs.findIndex(
513
+ (input, index) => !claimedInputs.has(index) && input.vaultRef === vaultRef
514
+ );
515
+ if (inputIndex >= 0)
516
+ claimedInputs.add(inputIndex);
517
+ return createUploadedPendingFile(file, inputIndex >= 0 ? fileInputs[inputIndex]?.name : void 0);
518
+ });
519
+ pendingFiles.value = [...pendingFiles.value, ...restoredFiles];
520
+ }
473
521
  function setDraft(text) {
474
522
  content.value = text;
475
523
  if (editorRef.value) {
@@ -488,6 +536,7 @@ function setDraft(text) {
488
536
  }
489
537
  defineExpose({
490
538
  addFiles,
539
+ restoreDraft,
491
540
  setDraft
492
541
  });
493
542
  </script>
@@ -641,7 +690,7 @@ defineExpose({
641
690
  v-for="pendingFile in pendingFiles"
642
691
  :key="pendingFile.id"
643
692
  :name="pendingFile.file.name"
644
- :icon="getFileIcon(pendingFile.file.type)"
693
+ :icon="getFileIcon(getPendingFileMimeType(pendingFile))"
645
694
  :meta="formatFileSize(pendingFile.file.size)"
646
695
  remove-label="Remover anexo"
647
696
  @remove="removeFile(pendingFile.id)"
@@ -1,8 +1,8 @@
1
1
  import type { DeepReadonly } from 'vue';
2
2
  import type { Message } from '../../types/schemas/chat/conversations.js';
3
- import type { ConversationFile } from '../../types/schemas/chat/files.js';
3
+ import type { ConversationFile, FileUploadInput } from '../../types/schemas/chat/files.js';
4
4
  import type { WorkspaceSettingsResponse as WorkspaceSettings } from '../../types/schemas/chat/workspace-settings.js';
5
- import type { TelaAgentContextFile, TelaAgentInputVariable, TelaAgentSkill } from '../../types/schemas/integrations/tela-agent-api.js';
5
+ import type { TelaAgentContextFile, TelaAgentExecutionInput, TelaAgentInputVariable, TelaAgentSkill } from '../../types/schemas/integrations/tela-agent-api.js';
6
6
  type __VLS_Props = {
7
7
  disabled?: boolean;
8
8
  loading?: boolean;
@@ -21,6 +21,11 @@ type __VLS_Props = {
21
21
  showCancelButton?: boolean;
22
22
  };
23
23
  declare function addFiles(files: File[], inputName?: string): void;
24
+ declare function restoreDraft(draft: {
25
+ content: string;
26
+ files?: FileUploadInput[];
27
+ agentInputs?: TelaAgentExecutionInput[];
28
+ }): void;
24
29
  declare function setDraft(text: string): void;
25
30
  type __VLS_ModelProps = {
26
31
  'selectedModel'?: string | null;
@@ -28,6 +33,7 @@ type __VLS_ModelProps = {
28
33
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
29
34
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
30
35
  addFiles: typeof addFiles;
36
+ restoreDraft: typeof restoreDraft;
31
37
  setDraft: typeof setDraft;
32
38
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
33
39
  "update:selectedModel": (value: string | null) => any;
@@ -18,9 +18,6 @@ const skeletonMessages = [
18
18
  function scrollToBottom() {
19
19
  conversationRef.value?.scrollToBottom();
20
20
  }
21
- const hasPendingMessage = computed(
22
- () => props.messages.some((message) => message.status === "pending")
23
- );
24
21
  watch(
25
22
  () => [props.messages.length, props.messages.at(-1)?.content.length ?? 0],
26
23
  scrollToBottom
@@ -36,7 +33,7 @@ onMounted(scrollToBottom);
36
33
  'pt-72px pb-24px px-16px': variant === 'mobile'
37
34
  }"
38
35
  >
39
- <TelaChatConversationContent class="max-w-700px!">
36
+ <TelaChatConversationContent class="max-w-700px! min-h-full">
40
37
  <template v-if="loading">
41
38
  <div
42
39
  v-for="message in skeletonMessages"
@@ -59,7 +56,8 @@ onMounted(scrollToBottom);
59
56
  @retry="emit('retry', $event)"
60
57
  />
61
58
  <p
62
- v-if="messages.length > 0 && !hasPendingMessage"
59
+ v-if="messages.length > 0"
60
+ mt-auto mb--6px
63
61
  text-center body-11-regular text-tertiary
64
62
  >
65
63
  As respostas podem conter erros. Verifique documentos originais.
@@ -24,7 +24,7 @@ defineExpose({
24
24
  </script>
25
25
 
26
26
  <template>
27
- <div v-if="groupedReasoning.length > 0" flex="~ col" w-full p-12px>
27
+ <div v-if="groupedReasoning.length > 0" flex="~ col" w-full>
28
28
  <ChatReasoningMessage
29
29
  v-for="(step, index) in groupedReasoning"
30
30
  :key="getStepKey(step, index)"