@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
@@ -14,17 +14,15 @@ const SUCCESS_ANIMATION_DURATION_MS = 1e3;
14
14
  const DOT_ANIMATION_INTERVAL_MS = 500;
15
15
  const AUTO_COLLAPSE_DELAY_MS = 1500;
16
16
  const TEXT_TRUNCATE_LENGTH = 40;
17
- const isExpanded = ref(false);
17
+ const isExpanded = ref(!props.completed);
18
18
  const userHasExpanded = ref(false);
19
19
  const showingSuccess = ref(false);
20
20
  const wasEverPending = ref(!props.completed);
21
- const previewContainer = ref(null);
22
21
  const dotCount = ref(0);
23
22
  const elapsedTime = ref(0);
24
23
  let dotInterval;
25
24
  let timerInterval;
26
25
  const storageKey = computed(() => `reasoning-timer-${new Date(props.createdAt).getTime()}`);
27
- const showGradient = computed(() => !props.completed && !isExpanded.value);
28
26
  const displayedReasoning = computed(
29
27
  () => resolveDisplayedReasoning(props.reasoningData, props.preparingSteps)
30
28
  );
@@ -74,26 +72,11 @@ const loadingText = computed(() => {
74
72
  const dots = ".".repeat(dotCount.value);
75
73
  return `${currentStepName.value}${dots}`;
76
74
  });
77
- function toggleExpanded() {
78
- userHasExpanded.value = true;
79
- isExpanded.value = !isExpanded.value;
80
- }
81
- function scheduleScroll() {
82
- requestAnimationFrame(() => {
83
- if (!previewContainer.value)
84
- return;
85
- previewContainer.value.scrollTop = previewContainer.value.scrollHeight;
86
- });
87
- }
88
- watch(() => groupedReasoning.value.length, () => {
89
- if (props.completed || isExpanded.value || !previewContainer.value) {
90
- return;
91
- }
92
- scheduleScroll();
93
- });
94
75
  watch(() => props.completed, (newCompleted, oldCompleted) => {
95
76
  if (!newCompleted) {
96
77
  wasEverPending.value = true;
78
+ if (!userHasExpanded.value)
79
+ isExpanded.value = true;
97
80
  }
98
81
  if (newCompleted && !oldCompleted && wasEverPending.value) {
99
82
  showingSuccess.value = true;
@@ -101,19 +84,13 @@ watch(() => props.completed, (newCompleted, oldCompleted) => {
101
84
  showingSuccess.value = false;
102
85
  if (!userHasExpanded.value) {
103
86
  setTimeout(() => {
104
- isExpanded.value = false;
87
+ if (props.completed)
88
+ isExpanded.value = false;
105
89
  }, AUTO_COLLAPSE_DELAY_MS);
106
90
  }
107
91
  }, SUCCESS_ANIMATION_DURATION_MS);
108
92
  }
109
93
  });
110
- watch(isExpanded, (newExpanded, oldExpanded) => {
111
- if (!newExpanded && oldExpanded) {
112
- nextTick(() => {
113
- scheduleScroll();
114
- });
115
- }
116
- });
117
94
  watch(() => props.completed, (isCompleted) => {
118
95
  if (!isCompleted && !dotInterval) {
119
96
  dotInterval = setInterval(() => {
@@ -150,11 +127,6 @@ onMounted(() => {
150
127
  elapsedTime.value = Date.now() - startTime;
151
128
  }
152
129
  }
153
- if (hasReasoning.value && !isExpanded.value) {
154
- nextTick(() => {
155
- scheduleScroll();
156
- });
157
- }
158
130
  });
159
131
  onUnmounted(() => {
160
132
  if (dotInterval)
@@ -209,14 +181,15 @@ onUnmounted(() => {
209
181
  key="loading-text"
210
182
  class="body-14-regular flex items-center gap-8px"
211
183
  >
212
- <span
213
- :class="hasReasoning ? 'text-gray-700' : 'running-gradient-text'"
214
- >
215
- {{ hasReasoning ? currentStepName : loadingText }}
184
+ <span v-if="hasReasoning" class="text-gray-700">
185
+ {{ currentStepName }}
216
186
  </span>
187
+ <TelaChatTextShimmer v-else>
188
+ {{ loadingText }}
189
+ </TelaChatTextShimmer>
217
190
  <span
218
191
  v-if="liveFormattedDuration"
219
- class="text-12px text-gray-500 timer-mono"
192
+ body-12-regular text-secondary
220
193
  >
221
194
  ({{ liveFormattedDuration }})
222
195
  </span>
@@ -234,34 +207,9 @@ onUnmounted(() => {
234
207
  </button>
235
208
  </TelaCollapsibleTrigger>
236
209
 
237
- <!-- Preview mode: Always visible during loading, with gradient -->
238
- <div
239
- v-if="!completed && !isExpanded"
240
- border="~ 0.5 default rounded-16px"
241
- mt-8px mb-16px
242
- cursor-pointer
243
- @click="toggleExpanded"
244
- >
245
- <div
246
- ref="previewContainer"
247
- class="preview-container"
248
- :class="{ 'with-gradient': showGradient }"
249
- >
250
- <ChatReasoningSteps
251
- :reasoning-data="displayedReasoning"
252
- :animate="animate && !completed"
253
- :completed="completed"
254
- :is-preview-mode="true"
255
- />
256
- </div>
257
- </div>
258
-
259
- <!-- Expanded mode: Full content without gradient -->
210
+ <!-- Open while running, collapsible once done -->
260
211
  <TelaCollapsibleContent>
261
- <div
262
- border="~ 0.5 default rounded-16px"
263
- mb-16px
264
- >
212
+ <div mb-8px>
265
213
  <ChatReasoningSteps
266
214
  :reasoning-data="displayedReasoning"
267
215
  :animate="animate && !completed"
@@ -274,5 +222,5 @@ onUnmounted(() => {
274
222
  </template>
275
223
 
276
224
  <style scoped>
277
- .running-gradient-text{background:linear-gradient(90deg,#9da2aa,#f4f5f7 50%,#9da2aa);background-clip:text;-webkit-background-clip:text;background-size:200% 100%;-webkit-text-fill-color:transparent;animation:gradientAnimation 1.5s linear infinite}.static-gradient-text{color:#9da2aa}.preview-container{max-height:153px;overflow-x:hidden;overflow-y:auto;position:relative;scrollbar-width:none;-ms-overflow-style:none}.preview-container.with-gradient{mask-image:linear-gradient(180deg,transparent 0,#000 60%,#000);-webkit-mask-image:linear-gradient(180deg,transparent 0,#000 60%,#000)}.preview-container::-webkit-scrollbar{display:none}@keyframes gradientAnimation{0%{background-position:200% 50%}to{background-position:0 50%}}.loading-dot{animation:pulse 1.5s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.85)}}.success-check{animation:successFade .2s ease-out}@keyframes successFade{0%{opacity:0}to{opacity:1}}.timer-mono{font-family:ui-monospace,SFMono-Regular,SF Mono,Monaco,Cascadia Mono,Segoe UI Mono,Roboto Mono,Oxygen Mono,"Ubuntu Monospace",Source Code Pro,Fira Mono,Droid Sans Mono,Courier New,monospace;font-variant-numeric:tabular-nums}
225
+ .static-gradient-text{color:#9da2aa}.loading-dot{animation:pulse 1.5s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.85)}}.success-check{animation:successFade .2s ease-out}@keyframes successFade{0%{opacity:0}to{opacity:1}}
278
226
  </style>
@@ -11,15 +11,15 @@ export function terminalMessageTransitions(previousMessages, nextMessages) {
11
11
  const transitions = [];
12
12
  for (const message of nextMessages) {
13
13
  const previous = previousById.get(message.id);
14
- if (message.role !== "assistant" || previous?.status !== "pending" /* Pending */)
14
+ if (message.role !== "assistant" || !previous)
15
15
  continue;
16
- if (message.status === "completed" /* Completed */) {
16
+ if (previous.status === "pending" /* Pending */ && message.status === "completed" /* Completed */) {
17
17
  transitions.push({
18
18
  conversationId: message.conversationId,
19
19
  messageId: message.id,
20
20
  status: "completed" /* Completed */
21
21
  });
22
- } else if (message.status === "failed" /* Failed */) {
22
+ } else if (message.status === "failed" /* Failed */ && (previous.status === "pending" /* Pending */ || previous.status === "completed" /* Completed */)) {
23
23
  transitions.push({
24
24
  conversationId: message.conversationId,
25
25
  messageId: message.id,
@@ -1,11 +1,16 @@
1
- import type { DeepReadonly } from 'vue';
1
+ import { type DeepReadonly, type MaybeRefOrGetter } from 'vue';
2
2
  import type { Conversation, ConversationWithMessages, Message } from '../../types/schemas/chat/conversations.js';
3
3
  import type { FileUploadInput } from '../../types/schemas/chat/files.js';
4
4
  import type { SendMessageBody } from '../../types/schemas/chat/messages.js';
5
5
  import type { TelaAgentExecutionInput } from '../../types/schemas/integrations/tela-agent-api.js';
6
+ import { type QueuedSend } from './outbound-queue.js';
6
7
  export declare function resolveTelaAgentInputs(messageInputs: TelaAgentExecutionInput[] | undefined, configuredInputs: DeepReadonly<TelaAgentExecutionInput[]> | null | undefined): SendMessageBody['agentInputs'];
7
8
  export declare function resolveStreamingContent(authoritative: string, provisional: string): string;
8
- export declare function useChat(): {
9
+ export declare function useChat(options?: {
10
+ outboundQueueEnabled?: MaybeRefOrGetter<boolean>;
11
+ onQueuedSendCancelled?: (send: QueuedSend) => void;
12
+ onQueuedSendFailed?: (send: QueuedSend) => void;
13
+ }): {
9
14
  currentConversation: any;
10
15
  messages: any;
11
16
  loading: any;
@@ -13,6 +18,12 @@ export declare function useChat(): {
13
18
  error: any;
14
19
  isPolling: any;
15
20
  hasPendingMessage: any;
21
+ composerLocked: any;
22
+ queuedMessageContent: any;
23
+ outboundOccupied: any;
24
+ sessionReady: any;
25
+ agentSessionStatus: any;
26
+ agentSessionRewakeable: any;
16
27
  streamingActiveMessageId: any;
17
28
  preparingSteps: any;
18
29
  cancelling: any;
@@ -25,7 +36,10 @@ export declare function useChat(): {
25
36
  pendingModelSelection: any;
26
37
  sendMessage: (content: string, files?: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[], prepare?: (signal: AbortSignal) => Promise<{
27
38
  wakeSessionId?: string;
28
- }>) => Promise<Message | null>;
39
+ defer?: boolean;
40
+ }>, queuedSend?: QueuedSend) => Promise<Message | null>;
41
+ flushOutboundQueue: (wakeSessionId?: string) => Promise<Message | null>;
42
+ failOutboundQueue: (message: string) => boolean;
29
43
  retryMessage: (messageId: string, wakeSessionId?: string) => Promise<boolean>;
30
44
  cancelConversation: () => Promise<boolean>;
31
45
  startPolling: () => void;
@@ -1,5 +1,6 @@
1
1
  import { useChatAuth } from "#chat-auth";
2
2
  import * as Sentry from "@sentry/nuxt";
3
+ import { toValue } from "vue";
3
4
  import { defaultChatModel } from "../../types/schemas/chat/models.js";
4
5
  import {
5
6
  createReasoningCache,
@@ -12,18 +13,20 @@ import {
12
13
  restoreAssistantMessageAfterRetryFailure,
13
14
  selectTitleGenerationMessages,
14
15
  shouldTreatLoadedConversationTitleAsGenerated,
15
- CANCELLATION_FAILED_TEXT
16
+ CANCELLATION_FAILED_TEXT,
17
+ GENERATION_FAILED_TEXT
16
18
  } from "../message-state.js";
17
19
  import { exportConversationToMarkdown } from "../../conversations/export.js";
18
20
  import { resolveChatStateScope } from "../../conversations/scope.js";
19
- import { getChatApiErrorCode, getChatApiErrorMessage } from "../../shared/chat-api-error.js";
21
+ import { getChatApiErrorCode, getChatApiErrorMessage, getChatApiErrorStatus } from "../../shared/chat-api-error.js";
20
22
  import { useChatApi } from "../../shared/composables/chat-api.js";
21
23
  import { useConversationFileActions } from "../../files/composables/conversation-files.js";
22
24
  import { useEmbedConfig } from "../../embed/composables/embed-config.js";
23
25
  import { CHAT_PREPARING_STEPS, nextChatPreparingStepDelay } from "../../embed/loading-messages.js";
24
26
  import { usePolling } from "../../shared/composables/polling.js";
25
- import { terminalMessageTransitions, useChatMessageTerminal } from "./chat-message-terminal.js";
27
+ import { ChatMessageStatus, terminalMessageTransitions, useChatMessageTerminal } from "./chat-message-terminal.js";
26
28
  import { useDirectStream } from "./use-direct-stream.js";
29
+ import { useOutboundQueue } from "./outbound-queue.js";
27
30
  const WORKSPACE_AGENT_NOT_READY_TEXT = "O agente deste workspace ainda n\xE3o foi configurado no Tela. Abra as configura\xE7\xF5es do workspace para criar o agente.";
28
31
  export function resolveTelaAgentInputs(messageInputs, configuredInputs) {
29
32
  const inputs = messageInputs ?? configuredInputs;
@@ -36,7 +39,7 @@ export function resolveStreamingContent(authoritative, provisional) {
36
39
  return authoritative;
37
40
  return authoritative || provisional;
38
41
  }
39
- export function useChat() {
42
+ export function useChat(options = {}) {
40
43
  const chatApi = useChatApi();
41
44
  const chatMessageTerminal = useChatMessageTerminal();
42
45
  const embedConfig = useEmbedConfig();
@@ -67,6 +70,23 @@ export function useChat() {
67
70
  };
68
71
  }
69
72
  });
73
+ const agentSessionStatusBuckets = useState("chat-agent-session-status", () => ({}));
74
+ const agentSessionExecutableBuckets = useState("chat-agent-session-executable", () => ({}));
75
+ const agentSessionRewakeableBuckets = useState("chat-agent-session-rewakeable", () => ({}));
76
+ const agentSessionStatus = computed({
77
+ get: () => agentSessionStatusBuckets.value[chatScope.value] ?? null,
78
+ set: (value) => agentSessionStatusBuckets.value = { ...agentSessionStatusBuckets.value, [chatScope.value]: value }
79
+ });
80
+ const agentSessionExecutable = computed({
81
+ get: () => agentSessionExecutableBuckets.value[chatScope.value] ?? false,
82
+ set: (value) => agentSessionExecutableBuckets.value = { ...agentSessionExecutableBuckets.value, [chatScope.value]: value }
83
+ });
84
+ const agentSessionRewakeable = computed({
85
+ get: () => agentSessionRewakeableBuckets.value[chatScope.value] ?? false,
86
+ set: (value) => agentSessionRewakeableBuckets.value = { ...agentSessionRewakeableBuckets.value, [chatScope.value]: value }
87
+ });
88
+ const outbound = useOutboundQueue(chatScope);
89
+ const outboundQueueEnabled = computed(() => toValue(options.outboundQueueEnabled) ?? false);
70
90
  const loading = ref(false);
71
91
  const sending = ref(false);
72
92
  const gateHolding = ref(false);
@@ -104,11 +124,49 @@ export function useChat() {
104
124
  const hasPendingMessage = computed(
105
125
  () => messages.value.some((m) => m.status === "pending")
106
126
  );
127
+ const sessionReady = computed(() => {
128
+ if (!currentConversation.value?.threadSessionId)
129
+ return null;
130
+ return agentSessionStatus.value === "waiting_messages" && agentSessionExecutable.value;
131
+ });
132
+ const composerLocked = computed(() => hasPendingMessage.value || outbound.occupied.value);
133
+ const queuedMessageContent = computed(() => outbound.slot.value?.content ?? null);
107
134
  const streamingMessageId = ref(null);
135
+ const pendingStreamReconciliationIds = ref(/* @__PURE__ */ new Set());
108
136
  let requestDurableRecovery = () => {
109
137
  };
138
+ function optimisticallyCompleteStreamedMessage(result) {
139
+ if (result.status === "waiting_messages") {
140
+ agentSessionStatus.value = "waiting_messages";
141
+ agentSessionExecutable.value = true;
142
+ agentSessionRewakeable.value = false;
143
+ }
144
+ const messageId = streamingMessageId.value;
145
+ if (!messageId)
146
+ return;
147
+ const pendingMessage = messages.value.find(
148
+ (message) => message.id === messageId && message.role === "assistant" && message.status === ChatMessageStatus.Pending
149
+ );
150
+ if (!pendingMessage)
151
+ return;
152
+ pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set([
153
+ ...pendingStreamReconciliationIds.value,
154
+ messageId
155
+ ]);
156
+ messages.value = messages.value.map((message) => message.id === messageId ? {
157
+ ...message,
158
+ content: resolveStreamingContent(message.content, result.text),
159
+ status: ChatMessageStatus.Completed
160
+ } : message);
161
+ chatMessageTerminal.emit({
162
+ conversationId: pendingMessage.conversationId,
163
+ messageId,
164
+ status: ChatMessageStatus.Completed
165
+ });
166
+ }
110
167
  const directStream = useDirectStream({
111
- onResult: () => requestDurableRecovery(),
168
+ onResult: optimisticallyCompleteStreamedMessage,
169
+ onTerminal: () => requestDurableRecovery(),
112
170
  onDisconnect: () => requestDurableRecovery()
113
171
  });
114
172
  const streamingActiveMessageId = computed(
@@ -121,12 +179,16 @@ export function useChat() {
121
179
  const content = directStream.streamingText.value;
122
180
  if (!messageId || !content)
123
181
  return resolved;
124
- return resolved.map((message) => message.id === messageId && message.status === "pending" ? { ...message, content: resolveStreamingContent(message.content, content) } : message);
182
+ return resolved.map((message) => message.id === messageId && (message.status === "pending" || pendingStreamReconciliationIds.value.has(message.id)) ? { ...message, content: resolveStreamingContent(message.content, content) } : message);
125
183
  });
126
- const stopCondition = computed(() => !hasPendingMessage.value);
184
+ const stopCondition = computed(
185
+ () => !hasPendingMessage.value && pendingStreamReconciliationIds.value.size === 0
186
+ );
127
187
  let pollGeneration = 0;
128
188
  const pollMessages = async () => {
129
- if (!currentConversation.value || gateHolding.value)
189
+ if (!currentConversation.value)
190
+ return;
191
+ if (gateHolding.value && !outboundQueueEnabled.value)
130
192
  return;
131
193
  const generation = pollGeneration;
132
194
  try {
@@ -136,9 +198,34 @@ export function useChat() {
136
198
  );
137
199
  if (generation !== pollGeneration)
138
200
  return;
139
- const terminalMessages = terminalMessageTransitions(messages.value, response.messages);
140
- messages.value = response.messages;
141
- const latestSessionId = response.messages.findLast((message) => message.agentSessionId)?.agentSessionId;
201
+ if (response.agentSession) {
202
+ agentSessionStatus.value = response.agentSession.status;
203
+ agentSessionExecutable.value = response.agentSession.executable;
204
+ agentSessionRewakeable.value = response.agentSession.rewakeable ?? false;
205
+ }
206
+ if (gateHolding.value)
207
+ return;
208
+ const currentMessagesById = new Map(messages.value.map((message) => [message.id, message]));
209
+ const remainingReconciliationIds = new Set(pendingStreamReconciliationIds.value);
210
+ const durableMessageIds = new Set(response.messages.map((message) => message.id));
211
+ for (const messageId of remainingReconciliationIds) {
212
+ if (!durableMessageIds.has(messageId))
213
+ remainingReconciliationIds.delete(messageId);
214
+ }
215
+ const reconciledMessages = response.messages.map((message) => {
216
+ if (!remainingReconciliationIds.has(message.id))
217
+ return message;
218
+ if (message.status === ChatMessageStatus.Completed || message.status === ChatMessageStatus.Failed) {
219
+ remainingReconciliationIds.delete(message.id);
220
+ return message;
221
+ }
222
+ const optimisticMessage = currentMessagesById.get(message.id);
223
+ return optimisticMessage?.status === ChatMessageStatus.Completed ? optimisticMessage : message;
224
+ });
225
+ const terminalMessages = terminalMessageTransitions(messages.value, reconciledMessages);
226
+ pendingStreamReconciliationIds.value = remainingReconciliationIds;
227
+ messages.value = reconciledMessages;
228
+ const latestSessionId = reconciledMessages.findLast((message) => message.agentSessionId)?.agentSessionId;
142
229
  if (latestSessionId && currentConversation.value.threadSessionId === null) {
143
230
  currentConversation.value = {
144
231
  ...currentConversation.value,
@@ -148,7 +235,7 @@ export function useChat() {
148
235
  for (const terminalMessage of terminalMessages)
149
236
  chatMessageTerminal.emit(terminalMessage);
150
237
  if (response.newFiles && response.newFiles.length > 0) {
151
- const lastAssistantMessage = response.messages.findLast((m) => m.role === "assistant");
238
+ const lastAssistantMessage = reconciledMessages.findLast((m) => m.role === "assistant");
152
239
  if (lastAssistantMessage) {
153
240
  addFilesToConversation(
154
241
  currentConversation.value.id,
@@ -165,7 +252,22 @@ export function useChat() {
165
252
  const errorRecord = err && typeof err === "object" ? err : null;
166
253
  const statusCode = typeof errorRecord?.statusCode === "number" ? errorRecord.statusCode : typeof errorRecord?.status === "number" ? errorRecord.status : typeof errorRecord?.response?.status === "number" ? errorRecord.response.status : null;
167
254
  if (statusCode && statusCode >= 400 && statusCode < 500 && statusCode !== 408 && statusCode !== 429) {
168
- messages.value = markPendingAssistantMessagesFailed(messages.value);
255
+ const previousMessages = messages.value;
256
+ const failedMessages = markPendingAssistantMessagesFailed(previousMessages).map((message) => {
257
+ if (message.role !== "assistant" || message.status !== ChatMessageStatus.Completed || !pendingStreamReconciliationIds.value.has(message.id)) {
258
+ return message;
259
+ }
260
+ return {
261
+ ...message,
262
+ content: GENERATION_FAILED_TEXT,
263
+ status: ChatMessageStatus.Failed
264
+ };
265
+ });
266
+ const terminalMessages = terminalMessageTransitions(previousMessages, failedMessages);
267
+ pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set();
268
+ messages.value = failedMessages;
269
+ for (const terminalMessage of terminalMessages)
270
+ chatMessageTerminal.emit(terminalMessage);
169
271
  error.value = getChatApiErrorMessage(err) ?? "Erro ao atualizar mensagens";
170
272
  stopPolling();
171
273
  }
@@ -219,6 +321,10 @@ export function useChat() {
219
321
  const conversationMessages = "messages" in conv ? conv.messages : [];
220
322
  currentConversation.value = { ...conv, messages: conversationMessages };
221
323
  messages.value = conversationMessages;
324
+ pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set();
325
+ agentSessionStatus.value = null;
326
+ agentSessionExecutable.value = false;
327
+ agentSessionRewakeable.value = false;
222
328
  titleGenerated.value = false;
223
329
  error.value = null;
224
330
  };
@@ -233,6 +339,7 @@ export function useChat() {
233
339
  );
234
340
  currentConversation.value = data;
235
341
  messages.value = data.messages;
342
+ pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set();
236
343
  if (shouldTreatLoadedConversationTitleAsGenerated(data.messages)) {
237
344
  titleGenerated.value = true;
238
345
  }
@@ -255,16 +362,20 @@ export function useChat() {
255
362
  loading.value = false;
256
363
  }
257
364
  };
258
- const sendMessage = async (content, files, agentInputs, prepare) => {
365
+ const sendMessage = async (content, files, agentInputs, prepare, queuedSend) => {
259
366
  if (!currentConversation.value && !prepare)
260
367
  return null;
368
+ if (outboundQueueEnabled.value && outbound.occupied.value && !queuedSend)
369
+ return null;
261
370
  sending.value = true;
262
371
  error.value = null;
263
372
  sendStartedAt = Date.now();
264
373
  schedulePreparingSteps();
265
374
  const filesWithSource = files?.map((f) => ({ ...f, source: "user" })) ?? null;
266
375
  const conversationId = currentConversation.value?.id ?? "";
267
- const optimisticUserMessage = {
376
+ const existingOptimisticUserMessage = queuedSend ? messages.value.find((message) => message.id === queuedSend.optimisticIds.user) : void 0;
377
+ const existingOptimisticAssistantMessage = queuedSend ? messages.value.find((message) => message.id === queuedSend.optimisticIds.assistant) : void 0;
378
+ const optimisticUserMessage = existingOptimisticUserMessage ?? {
268
379
  id: crypto.randomUUID(),
269
380
  conversationId,
270
381
  role: "user",
@@ -278,7 +389,7 @@ export function useChat() {
278
389
  createdAt: /* @__PURE__ */ new Date(),
279
390
  updatedAt: null
280
391
  };
281
- const optimisticAssistantMessage = {
392
+ const optimisticAssistantMessage = existingOptimisticAssistantMessage ?? {
282
393
  id: crypto.randomUUID(),
283
394
  conversationId,
284
395
  role: "assistant",
@@ -294,15 +405,19 @@ export function useChat() {
294
405
  };
295
406
  streamingMessageId.value = optimisticAssistantMessage.id;
296
407
  directStream.beginMessage();
297
- messages.value = [...messages.value, optimisticUserMessage, optimisticAssistantMessage];
408
+ if (!queuedSend)
409
+ messages.value = [...messages.value, optimisticUserMessage, optimisticAssistantMessage];
298
410
  const controller = new AbortController();
299
411
  gateController = controller;
300
412
  try {
301
- let wakeSessionId;
413
+ let wakeSessionId = queuedSend?.wakeSessionId;
414
+ let defer = false;
302
415
  if (prepare) {
303
416
  gateHolding.value = true;
304
417
  try {
305
- wakeSessionId = (await prepare(controller.signal)).wakeSessionId;
418
+ const prepared = await prepare(controller.signal);
419
+ wakeSessionId = prepared.wakeSessionId;
420
+ defer = prepared.defer === true;
306
421
  } finally {
307
422
  gateHolding.value = false;
308
423
  }
@@ -311,6 +426,21 @@ export function useChat() {
311
426
  throw new DOMException("Aborted", "AbortError");
312
427
  if (!currentConversation.value)
313
428
  throw new Error("Erro ao enviar mensagem");
429
+ const sessionReady2 = currentConversation.value.threadSessionId ? agentSessionStatus.value === "waiting_messages" && agentSessionExecutable.value : null;
430
+ if (outboundQueueEnabled.value && !queuedSend && (defer || sessionReady2 === false)) {
431
+ outbound.enqueue({
432
+ content,
433
+ files,
434
+ agentInputs,
435
+ wakeSessionId,
436
+ optimisticIds: { user: optimisticUserMessage.id, assistant: optimisticAssistantMessage.id },
437
+ queuedAt: Date.now(),
438
+ attempts: 0
439
+ });
440
+ gateHolding.value = true;
441
+ startPolling();
442
+ return optimisticUserMessage;
443
+ }
314
444
  const resolvedAgentInputs = resolveTelaAgentInputs(agentInputs, embedConfig?.telaAgentInputs?.value);
315
445
  const body = {
316
446
  content,
@@ -347,7 +477,10 @@ export function useChat() {
347
477
  acceptMessagePair
348
478
  );
349
479
  acceptedByStream = true;
350
- } catch {
480
+ } catch (streamError) {
481
+ const statusCode = getChatApiErrorStatus(streamError);
482
+ if (statusCode !== null && statusCode >= 400 && statusCode < 500)
483
+ throw streamError;
351
484
  response = await $fetch(
352
485
  chatApi.path(`/conversations/${currentConversation.value.id}/messages`),
353
486
  chatApi.withChatHeaders({ method: "POST", body })
@@ -366,6 +499,30 @@ export function useChat() {
366
499
  startPolling();
367
500
  return response.userMessage;
368
501
  } catch (err) {
502
+ const code = getChatApiErrorCode(err);
503
+ const queuedAttempts = (queuedSend?.attempts ?? 0) + 1;
504
+ const queuedAgeMs = Date.now() - (queuedSend?.queuedAt ?? Date.now());
505
+ if (outboundQueueEnabled.value && (code === "turn_in_progress" || code === "sandbox_not_woken") && queuedAttempts < 3 && queuedAgeMs <= 9e4) {
506
+ outbound.replace({
507
+ content,
508
+ files,
509
+ agentInputs,
510
+ wakeSessionId: queuedSend?.wakeSessionId,
511
+ optimisticIds: { user: optimisticUserMessage.id, assistant: optimisticAssistantMessage.id },
512
+ queuedAt: queuedSend?.queuedAt ?? Date.now(),
513
+ attempts: queuedAttempts
514
+ });
515
+ if (code === "sandbox_not_woken") {
516
+ agentSessionExecutable.value = false;
517
+ agentSessionRewakeable.value = true;
518
+ } else {
519
+ agentSessionStatus.value = "running";
520
+ agentSessionExecutable.value = false;
521
+ }
522
+ gateHolding.value = true;
523
+ startPolling();
524
+ return optimisticUserMessage;
525
+ }
369
526
  messages.value = messages.value.filter(
370
527
  (msg) => msg.id !== optimisticUserMessage.id && msg.id !== optimisticAssistantMessage.id
371
528
  );
@@ -377,27 +534,84 @@ export function useChat() {
377
534
  return null;
378
535
  const message = getChatApiErrorCode(err) === "workspace_agent_not_ready" ? WORKSPACE_AGENT_NOT_READY_TEXT : err instanceof Error ? err.message : "Erro ao enviar mensagem";
379
536
  error.value = message;
537
+ if (outboundQueueEnabled.value && queuedSend)
538
+ options.onQueuedSendFailed?.(queuedSend);
380
539
  Sentry.captureException(err);
381
540
  return null;
382
541
  } finally {
383
542
  sending.value = false;
384
- gateHolding.value = false;
543
+ gateHolding.value = outbound.occupied.value;
385
544
  if (gateController === controller)
386
545
  gateController = null;
387
546
  }
388
547
  };
548
+ let outboundFlushInFlight = null;
549
+ function flushOutboundQueue(wakeSessionId) {
550
+ if (outboundFlushInFlight)
551
+ return outboundFlushInFlight;
552
+ const queued = outbound.take();
553
+ if (!queued)
554
+ return Promise.resolve(null);
555
+ gateHolding.value = false;
556
+ const send = wakeSessionId ? { ...queued, wakeSessionId } : queued;
557
+ const pending = sendMessage(send.content, send.files, send.agentInputs, void 0, send);
558
+ outboundFlushInFlight = pending;
559
+ void pending.finally(() => {
560
+ if (outboundFlushInFlight === pending)
561
+ outboundFlushInFlight = null;
562
+ });
563
+ return pending;
564
+ }
565
+ function failOutboundQueue(message) {
566
+ const queued = outbound.clear();
567
+ if (!queued)
568
+ return false;
569
+ messages.value = messages.value.filter(
570
+ (item) => item.id !== queued.optimisticIds.user && item.id !== queued.optimisticIds.assistant
571
+ );
572
+ gateHolding.value = false;
573
+ streamingMessageId.value = null;
574
+ clearPreparingSteps();
575
+ directStream.disconnect();
576
+ stopPolling();
577
+ error.value = message;
578
+ options.onQueuedSendFailed?.(queued);
579
+ return true;
580
+ }
581
+ watch(sessionReady, (ready) => {
582
+ if (ready === true && outbound.occupied.value)
583
+ void flushOutboundQueue();
584
+ });
389
585
  const clearConversation = () => {
390
586
  stopPolling();
391
587
  gateController?.abort();
392
588
  streamingMessageId.value = null;
393
589
  clearPreparingSteps();
394
590
  directStream.disconnect();
591
+ outbound.clear();
395
592
  currentConversation.value = null;
396
593
  messages.value = [];
594
+ pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set();
397
595
  error.value = null;
596
+ agentSessionStatus.value = null;
597
+ agentSessionExecutable.value = false;
598
+ agentSessionRewakeable.value = false;
398
599
  };
399
600
  const cancelling = ref(false);
400
601
  const cancelConversation = async () => {
602
+ const queued = outbound.clear();
603
+ if (queued) {
604
+ messages.value = messages.value.filter(
605
+ (message) => message.id !== queued.optimisticIds.user && message.id !== queued.optimisticIds.assistant
606
+ );
607
+ gateHolding.value = false;
608
+ streamingMessageId.value = null;
609
+ clearPreparingSteps();
610
+ directStream.disconnect();
611
+ stopPolling();
612
+ options.onQueuedSendCancelled?.(queued);
613
+ return true;
614
+ }
401
615
  if (gateHolding.value) {
402
616
  gateController?.abort();
403
617
  return true;
@@ -445,6 +659,7 @@ export function useChat() {
445
659
  messages: []
446
660
  };
447
661
  messages.value = [];
662
+ pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set();
448
663
  streamingMessageId.value = null;
449
664
  directStream.disconnect();
450
665
  return true;
@@ -548,8 +763,8 @@ export function useChat() {
548
763
  messages: messages.value
549
764
  });
550
765
  };
551
- watch(hasPendingMessage, (isPending, wasPending) => {
552
- if (wasPending && !isPending) {
766
+ watch(stopCondition, (settled, wasSettled) => {
767
+ if (settled && !wasSettled) {
553
768
  streamingMessageId.value = null;
554
769
  directStream.disconnect();
555
770
  void generateTitle();
@@ -566,6 +781,12 @@ export function useChat() {
566
781
  error: readonly(error),
567
782
  isPolling: readonly(isPolling),
568
783
  hasPendingMessage,
784
+ composerLocked,
785
+ queuedMessageContent,
786
+ outboundOccupied: outbound.occupied,
787
+ sessionReady,
788
+ agentSessionStatus: readonly(agentSessionStatus),
789
+ agentSessionRewakeable: readonly(agentSessionRewakeable),
569
790
  streamingActiveMessageId,
570
791
  preparingSteps: readonly(preparingSteps),
571
792
  cancelling: readonly(cancelling),
@@ -577,6 +798,8 @@ export function useChat() {
577
798
  setConversationModel,
578
799
  pendingModelSelection,
579
800
  sendMessage,
801
+ flushOutboundQueue,
802
+ failOutboundQueue,
580
803
  retryMessage,
581
804
  cancelConversation,
582
805
  startPolling,