@meistrari/chat-nuxt 3.10.0 → 3.11.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 (35) hide show
  1. package/README.md +1 -1
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/embed/components/chat-embed-inner.vue +93 -38
  4. package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue +0 -1
  5. package/dist/runtime/embed/composables/use-agent-wake.d.ts +23 -0
  6. package/dist/runtime/embed/composables/use-agent-wake.js +182 -0
  7. package/dist/runtime/embed/feature-config.d.ts +4 -4
  8. package/dist/runtime/embed/feature-config.js +2 -1
  9. package/dist/runtime/embed/loading-messages.d.ts +16 -0
  10. package/dist/runtime/embed/loading-messages.js +10 -0
  11. package/dist/runtime/messages/components/chat-message-bubble.d.vue.ts +10 -1
  12. package/dist/runtime/messages/components/chat-message-bubble.vue +18 -7
  13. package/dist/runtime/messages/components/chat-message-bubble.vue.d.ts +10 -1
  14. package/dist/runtime/messages/components/chat-message-list.d.vue.ts +6 -1
  15. package/dist/runtime/messages/components/chat-message-list.vue +11 -2
  16. package/dist/runtime/messages/components/chat-message-list.vue.d.ts +6 -1
  17. package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.d.vue.ts +2 -1
  18. package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue +9 -8
  19. package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue.d.ts +2 -1
  20. package/dist/runtime/messages/composables/chat.d.ts +8 -2
  21. package/dist/runtime/messages/composables/chat.js +180 -25
  22. package/dist/runtime/messages/composables/use-direct-stream.d.ts +41 -0
  23. package/dist/runtime/messages/composables/use-direct-stream.js +263 -0
  24. package/dist/runtime/messages/reasoning.d.ts +7 -0
  25. package/dist/runtime/messages/reasoning.js +5 -0
  26. package/dist/runtime/types/schemas/chat/conversations.d.ts +2 -0
  27. package/dist/runtime/types/schemas/chat/messages.d.ts +39 -0
  28. package/dist/runtime/types/schemas/chat/messages.js +24 -0
  29. package/dist/runtime/types/schemas/chat/models.d.ts +3 -2
  30. package/dist/runtime/types/schemas/chat/models.js +2 -1
  31. package/dist/runtime/types/schemas/chat/ui.d.ts +10 -0
  32. package/dist/runtime/types/schemas/chat/ui.js +1 -0
  33. package/dist/runtime/types/schemas/integrations/tela-agent-api.d.ts +35 -0
  34. package/dist/runtime/types/schemas/integrations/tela-agent-api.js +17 -0
  35. package/package.json +1 -1
@@ -1,11 +1,20 @@
1
1
  import type { Message } from '../../types/schemas/chat/conversations.js';
2
2
  type __VLS_Props = {
3
3
  message: Message;
4
+ streamActive?: boolean;
5
+ /**
6
+ * Stand-in steps shown while this turn is still working towards its first
7
+ * output. They yield as soon as the run emits real reasoning or content.
8
+ */
9
+ preparingSteps?: readonly string[] | null;
10
+ showTtft?: boolean;
4
11
  };
5
12
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
6
13
  retry: (messageId: string) => any;
7
14
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
15
  onRetry?: ((messageId: string) => any) | undefined;
9
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ }>, {
17
+ showTtft: boolean;
18
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
19
  declare const _default: typeof __VLS_export;
11
20
  export default _default;
@@ -8,11 +8,16 @@ type __VLS_Props = {
8
8
  * own spacing. Defaults to `page`.
9
9
  */
10
10
  variant?: 'page' | 'widget' | 'mobile';
11
+ streamingActiveMessageId?: string | null;
12
+ preparingSteps?: readonly string[] | null;
13
+ showTtft?: boolean;
11
14
  };
12
15
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
13
16
  retry: (messageId: string) => any;
14
17
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
18
  onRetry?: ((messageId: string) => any) | undefined;
16
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ }>, {
20
+ showTtft: boolean;
21
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
22
  declare const _default: typeof __VLS_export;
18
23
  export default _default;
@@ -2,7 +2,10 @@
2
2
  const props = defineProps({
3
3
  messages: { type: Array, required: true },
4
4
  loading: { type: Boolean, required: false },
5
- variant: { type: String, required: false }
5
+ variant: { type: String, required: false },
6
+ streamingActiveMessageId: { type: [String, null], required: false },
7
+ preparingSteps: { type: [Array, null], required: false },
8
+ showTtft: { type: Boolean, required: false, default: true }
6
9
  });
7
10
  const emit = defineEmits(["retry"]);
8
11
  const conversationRef = ref(null);
@@ -18,7 +21,10 @@ function scrollToBottom() {
18
21
  const hasPendingMessage = computed(
19
22
  () => props.messages.some((message) => message.status === "pending")
20
23
  );
21
- watch(() => props.messages.length, scrollToBottom);
24
+ watch(
25
+ () => [props.messages.length, props.messages.at(-1)?.content.length ?? 0],
26
+ scrollToBottom
27
+ );
22
28
  onMounted(scrollToBottom);
23
29
  </script>
24
30
 
@@ -47,6 +53,9 @@ onMounted(scrollToBottom);
47
53
  v-for="message in messages"
48
54
  :key="message.id"
49
55
  :message="message"
56
+ :stream-active="message.id === streamingActiveMessageId"
57
+ :preparing-steps="message.role === 'assistant' && message.status === 'pending' ? preparingSteps : null"
58
+ :show-ttft="showTtft"
50
59
  @retry="emit('retry', $event)"
51
60
  />
52
61
  <p
@@ -8,11 +8,16 @@ type __VLS_Props = {
8
8
  * own spacing. Defaults to `page`.
9
9
  */
10
10
  variant?: 'page' | 'widget' | 'mobile';
11
+ streamingActiveMessageId?: string | null;
12
+ preparingSteps?: readonly string[] | null;
13
+ showTtft?: boolean;
11
14
  };
12
15
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
13
16
  retry: (messageId: string) => any;
14
17
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
18
  onRetry?: ((messageId: string) => any) | undefined;
16
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ }>, {
20
+ showTtft: boolean;
21
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
22
  declare const _default: typeof __VLS_export;
18
23
  export default _default;
@@ -1,10 +1,11 @@
1
- import type { ReasoningStep } from '../../reasoning.js';
1
+ import { type ReasoningStep } from '../../reasoning.js';
2
2
  type __VLS_Props = {
3
3
  reasoningData: ReasoningStep[];
4
4
  completed: boolean;
5
5
  animate?: boolean;
6
6
  createdAt: Date | string;
7
7
  updatedAt?: Date | string | null;
8
+ preparingSteps?: readonly string[] | null;
8
9
  };
9
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
11
  declare const _default: typeof __VLS_export;
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { resolveDisplayedReasoning } from "../../reasoning";
2
3
  import { resolveToolTitle } from "../../tool-registry";
3
4
  import ChatReasoningSteps from "./chat-reasoning-steps.vue";
4
5
  const props = defineProps({
@@ -6,7 +7,8 @@ const props = defineProps({
6
7
  completed: { type: Boolean, required: true },
7
8
  animate: { type: Boolean, required: false },
8
9
  createdAt: { type: [Date, String], required: true },
9
- updatedAt: { type: [Date, String, null], required: false }
10
+ updatedAt: { type: [Date, String, null], required: false },
11
+ preparingSteps: { type: [Array, null], required: false }
10
12
  });
11
13
  const SUCCESS_ANIMATION_DURATION_MS = 1e3;
12
14
  const DOT_ANIMATION_INTERVAL_MS = 500;
@@ -23,11 +25,10 @@ let dotInterval;
23
25
  let timerInterval;
24
26
  const storageKey = computed(() => `reasoning-timer-${new Date(props.createdAt).getTime()}`);
25
27
  const showGradient = computed(() => !props.completed && !isExpanded.value);
26
- const groupedReasoning = computed(() => {
27
- if (!props.reasoningData || !Array.isArray(props.reasoningData))
28
- return [];
29
- return props.reasoningData;
30
- });
28
+ const displayedReasoning = computed(
29
+ () => resolveDisplayedReasoning(props.reasoningData, props.preparingSteps)
30
+ );
31
+ const groupedReasoning = computed(() => displayedReasoning.value);
31
32
  const hasReasoning = computed(() => groupedReasoning.value.length > 0);
32
33
  const lastStep = computed(() => {
33
34
  const steps = groupedReasoning.value;
@@ -247,7 +248,7 @@ onUnmounted(() => {
247
248
  :class="{ 'with-gradient': showGradient }"
248
249
  >
249
250
  <ChatReasoningSteps
250
- :reasoning-data="reasoningData"
251
+ :reasoning-data="displayedReasoning"
251
252
  :animate="animate && !completed"
252
253
  :completed="completed"
253
254
  :is-preview-mode="true"
@@ -262,7 +263,7 @@ onUnmounted(() => {
262
263
  mb-16px
263
264
  >
264
265
  <ChatReasoningSteps
265
- :reasoning-data="reasoningData"
266
+ :reasoning-data="displayedReasoning"
266
267
  :animate="animate && !completed"
267
268
  :completed="completed"
268
269
  />
@@ -1,10 +1,11 @@
1
- import type { ReasoningStep } from '../../reasoning.js';
1
+ import { type ReasoningStep } from '../../reasoning.js';
2
2
  type __VLS_Props = {
3
3
  reasoningData: ReasoningStep[];
4
4
  completed: boolean;
5
5
  animate?: boolean;
6
6
  createdAt: Date | string;
7
7
  updatedAt?: Date | string | null;
8
+ preparingSteps?: readonly string[] | null;
8
9
  };
9
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
11
  declare const _default: typeof __VLS_export;
@@ -4,6 +4,7 @@ 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
6
  export declare function resolveTelaAgentInputs(messageInputs: TelaAgentExecutionInput[] | undefined, configuredInputs: DeepReadonly<TelaAgentExecutionInput[]> | null | undefined): SendMessageBody['agentInputs'];
7
+ export declare function resolveStreamingContent(authoritative: string, provisional: string): string;
7
8
  export declare function useChat(): {
8
9
  currentConversation: any;
9
10
  messages: any;
@@ -12,15 +13,20 @@ export declare function useChat(): {
12
13
  error: any;
13
14
  isPolling: any;
14
15
  hasPendingMessage: any;
16
+ streamingActiveMessageId: any;
17
+ preparingSteps: any;
15
18
  cancelling: any;
16
19
  generatingTitle: any;
17
20
  animatedTitle: any;
18
21
  loadConversation: (id: string) => Promise<void>;
19
22
  setConversation: (conv: Conversation | ConversationWithMessages) => void;
23
+ adoptConversation: (conv: Conversation) => void;
20
24
  setConversationModel: (model: string | null) => void;
21
25
  pendingModelSelection: any;
22
- sendMessage: (content: string, files?: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[]) => Promise<Message | null>;
23
- retryMessage: (messageId: string) => Promise<boolean>;
26
+ sendMessage: (content: string, files?: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[], prepare?: (signal: AbortSignal) => Promise<{
27
+ wakeSessionId?: string;
28
+ }>) => Promise<Message | null>;
29
+ retryMessage: (messageId: string, wakeSessionId?: string) => Promise<boolean>;
24
30
  cancelConversation: () => Promise<boolean>;
25
31
  startPolling: () => void;
26
32
  stopPolling: () => void;
@@ -20,13 +20,22 @@ import { getChatApiErrorCode, getChatApiErrorMessage } from "../../shared/chat-a
20
20
  import { useChatApi } from "../../shared/composables/chat-api.js";
21
21
  import { useConversationFileActions } from "../../files/composables/conversation-files.js";
22
22
  import { useEmbedConfig } from "../../embed/composables/embed-config.js";
23
+ import { CHAT_PREPARING_STEPS, nextChatPreparingStepDelay } from "../../embed/loading-messages.js";
23
24
  import { usePolling } from "../../shared/composables/polling.js";
24
25
  import { terminalMessageTransitions, useChatMessageTerminal } from "./chat-message-terminal.js";
26
+ import { useDirectStream } from "./use-direct-stream.js";
25
27
  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.";
26
28
  export function resolveTelaAgentInputs(messageInputs, configuredInputs) {
27
29
  const inputs = messageInputs ?? configuredInputs;
28
30
  return inputs === void 0 || inputs === null ? void 0 : inputs.map((input) => ({ ...input }));
29
31
  }
32
+ export function resolveStreamingContent(authoritative, provisional) {
33
+ if (provisional.startsWith(authoritative))
34
+ return provisional;
35
+ if (authoritative.startsWith(provisional))
36
+ return authoritative;
37
+ return authoritative || provisional;
38
+ }
30
39
  export function useChat() {
31
40
  const chatApi = useChatApi();
32
41
  const chatMessageTerminal = useChatMessageTerminal();
@@ -60,6 +69,34 @@ export function useChat() {
60
69
  });
61
70
  const loading = ref(false);
62
71
  const sending = ref(false);
72
+ const gateHolding = ref(false);
73
+ const preparingSteps = ref([]);
74
+ let preparingTimer;
75
+ let gateController = null;
76
+ let sendStartedAt = 0;
77
+ let standInOutputAtMs = null;
78
+ function clearPreparingSteps() {
79
+ if (preparingTimer)
80
+ clearTimeout(preparingTimer);
81
+ preparingTimer = void 0;
82
+ preparingSteps.value = [];
83
+ standInOutputAtMs = null;
84
+ }
85
+ function schedulePreparingStep(index) {
86
+ if (index >= CHAT_PREPARING_STEPS.length)
87
+ return;
88
+ const delay = nextChatPreparingStepDelay(index);
89
+ if (index === 0)
90
+ standInOutputAtMs = delay;
91
+ preparingTimer = setTimeout(() => {
92
+ preparingSteps.value = CHAT_PREPARING_STEPS.slice(0, index + 1);
93
+ schedulePreparingStep(index + 1);
94
+ }, delay);
95
+ }
96
+ function schedulePreparingSteps() {
97
+ clearPreparingSteps();
98
+ schedulePreparingStep(0);
99
+ }
63
100
  const error = ref(null);
64
101
  const titleGenerated = ref(false);
65
102
  const generatingTitle = ref(false);
@@ -67,12 +104,29 @@ export function useChat() {
67
104
  const hasPendingMessage = computed(
68
105
  () => messages.value.some((m) => m.status === "pending")
69
106
  );
107
+ const streamingMessageId = ref(null);
108
+ let requestDurableRecovery = () => {
109
+ };
110
+ const directStream = useDirectStream({
111
+ onResult: () => requestDurableRecovery(),
112
+ onDisconnect: () => requestDurableRecovery()
113
+ });
114
+ const streamingActiveMessageId = computed(
115
+ () => directStream.activity.value === "active" ? streamingMessageId.value : null
116
+ );
70
117
  const reasoningCache = createReasoningCache();
71
- const processedMessages = computed(() => resolveReasoningDisplayMessages(messages.value, reasoningCache));
118
+ const processedMessages = computed(() => {
119
+ const resolved = resolveReasoningDisplayMessages(messages.value, reasoningCache);
120
+ const messageId = streamingMessageId.value;
121
+ const content = directStream.streamingText.value;
122
+ if (!messageId || !content)
123
+ return resolved;
124
+ return resolved.map((message) => message.id === messageId && message.status === "pending" ? { ...message, content: resolveStreamingContent(message.content, content) } : message);
125
+ });
72
126
  const stopCondition = computed(() => !hasPendingMessage.value);
73
127
  let pollGeneration = 0;
74
128
  const pollMessages = async () => {
75
- if (!currentConversation.value)
129
+ if (!currentConversation.value || gateHolding.value)
76
130
  return;
77
131
  const generation = pollGeneration;
78
132
  try {
@@ -84,6 +138,13 @@ export function useChat() {
84
138
  return;
85
139
  const terminalMessages = terminalMessageTransitions(messages.value, response.messages);
86
140
  messages.value = response.messages;
141
+ const latestSessionId = response.messages.findLast((message) => message.agentSessionId)?.agentSessionId;
142
+ if (latestSessionId && currentConversation.value.threadSessionId === null) {
143
+ currentConversation.value = {
144
+ ...currentConversation.value,
145
+ threadSessionId: latestSessionId
146
+ };
147
+ }
87
148
  for (const terminalMessage of terminalMessages)
88
149
  chatMessageTerminal.emit(terminalMessage);
89
150
  if (response.newFiles && response.newFiles.length > 0) {
@@ -123,6 +184,10 @@ export function useChat() {
123
184
  pollGeneration++;
124
185
  endPolling();
125
186
  }
187
+ requestDurableRecovery = () => {
188
+ startPolling();
189
+ void pollMessages();
190
+ };
126
191
  const pendingModelBuckets = useState("chat-pending-model-selection", () => ({}));
127
192
  const pendingModelSelection = computed({
128
193
  get: () => {
@@ -142,7 +207,15 @@ export function useChat() {
142
207
  return;
143
208
  currentConversation.value = { ...conversation, model };
144
209
  };
210
+ const adoptConversation = (conv) => {
211
+ currentConversation.value = { ...conv, messages: messages.value };
212
+ titleGenerated.value = false;
213
+ };
145
214
  const setConversation = (conv) => {
215
+ if (currentConversation.value && currentConversation.value.id !== conv.id) {
216
+ directStream.disconnect();
217
+ streamingMessageId.value = null;
218
+ }
146
219
  const conversationMessages = "messages" in conv ? conv.messages : [];
147
220
  currentConversation.value = { ...conv, messages: conversationMessages };
148
221
  messages.value = conversationMessages;
@@ -163,8 +236,14 @@ export function useChat() {
163
236
  if (shouldTreatLoadedConversationTitleAsGenerated(data.messages)) {
164
237
  titleGenerated.value = true;
165
238
  }
166
- if (data.messages.some((m) => m.status === "pending")) {
239
+ const pendingAssistant = data.messages.findLast((m) => m.role === "assistant" && m.status === "pending");
240
+ if (pendingAssistant) {
241
+ streamingMessageId.value = pendingAssistant.id;
242
+ directStream.beginMessage();
167
243
  startPolling();
244
+ } else {
245
+ streamingMessageId.value = null;
246
+ directStream.disconnect();
168
247
  }
169
248
  } catch (err) {
170
249
  currentConversation.value = null;
@@ -176,15 +255,18 @@ export function useChat() {
176
255
  loading.value = false;
177
256
  }
178
257
  };
179
- const sendMessage = async (content, files, agentInputs) => {
180
- if (!currentConversation.value)
258
+ const sendMessage = async (content, files, agentInputs, prepare) => {
259
+ if (!currentConversation.value && !prepare)
181
260
  return null;
182
261
  sending.value = true;
183
262
  error.value = null;
263
+ sendStartedAt = Date.now();
264
+ schedulePreparingSteps();
184
265
  const filesWithSource = files?.map((f) => ({ ...f, source: "user" })) ?? null;
266
+ const conversationId = currentConversation.value?.id ?? "";
185
267
  const optimisticUserMessage = {
186
268
  id: crypto.randomUUID(),
187
- conversationId: currentConversation.value.id,
269
+ conversationId,
188
270
  role: "user",
189
271
  content,
190
272
  status: "completed",
@@ -198,7 +280,7 @@ export function useChat() {
198
280
  };
199
281
  const optimisticAssistantMessage = {
200
282
  id: crypto.randomUUID(),
201
- conversationId: currentConversation.value.id,
283
+ conversationId,
202
284
  role: "assistant",
203
285
  content: "",
204
286
  status: "pending",
@@ -210,28 +292,68 @@ export function useChat() {
210
292
  createdAt: /* @__PURE__ */ new Date(),
211
293
  updatedAt: null
212
294
  };
295
+ streamingMessageId.value = optimisticAssistantMessage.id;
296
+ directStream.beginMessage();
213
297
  messages.value = [...messages.value, optimisticUserMessage, optimisticAssistantMessage];
298
+ const controller = new AbortController();
299
+ gateController = controller;
214
300
  try {
301
+ let wakeSessionId;
302
+ if (prepare) {
303
+ gateHolding.value = true;
304
+ try {
305
+ wakeSessionId = (await prepare(controller.signal)).wakeSessionId;
306
+ } finally {
307
+ gateHolding.value = false;
308
+ }
309
+ }
310
+ if (controller.signal.aborted)
311
+ throw new DOMException("Aborted", "AbortError");
312
+ if (!currentConversation.value)
313
+ throw new Error("Erro ao enviar mensagem");
215
314
  const resolvedAgentInputs = resolveTelaAgentInputs(agentInputs, embedConfig?.telaAgentInputs?.value);
216
315
  const body = {
217
316
  content,
218
317
  files,
219
- ...resolvedAgentInputs !== void 0 ? { agentInputs: resolvedAgentInputs } : {}
318
+ ...resolvedAgentInputs !== void 0 ? { agentInputs: resolvedAgentInputs } : {},
319
+ ...wakeSessionId ? { wakeSessionId } : {},
320
+ clientTiming: {
321
+ // Everything the gate held goes in here: whatever the server
322
+ // measures, the user has already been waiting this long.
323
+ sinceSendMs: Math.max(0, Date.now() - sendStartedAt),
324
+ ...standInOutputAtMs !== null ? { standInOutputAtMs } : {}
325
+ }
220
326
  };
221
- const response = await $fetch(
222
- chatApi.path(`/conversations/${currentConversation.value.id}/messages`),
223
- chatApi.withChatHeaders({
224
- method: "POST",
225
- body
226
- })
227
- );
228
- messages.value = messages.value.map((msg) => {
229
- if (msg.id === optimisticUserMessage.id)
230
- return response.userMessage;
231
- if (msg.id === optimisticAssistantMessage.id)
232
- return response.assistantMessage;
233
- return msg;
234
- });
327
+ const acceptMessagePair = (response2) => {
328
+ messages.value = messages.value.map((msg) => {
329
+ if (msg.id === optimisticUserMessage.id)
330
+ return response2.userMessage;
331
+ if (msg.id === optimisticAssistantMessage.id)
332
+ return response2.assistantMessage;
333
+ return msg;
334
+ });
335
+ streamingMessageId.value = response2.assistantMessage.id;
336
+ };
337
+ let response;
338
+ let acceptedByStream = false;
339
+ try {
340
+ response = await directStream.connectMessage(
341
+ chatApi.path(`/conversations/${currentConversation.value.id}/messages/stream`),
342
+ chatApi.withChatHeaders({
343
+ method: "POST",
344
+ headers: { "Content-Type": "application/json" },
345
+ body: JSON.stringify(body)
346
+ }),
347
+ acceptMessagePair
348
+ );
349
+ acceptedByStream = true;
350
+ } catch {
351
+ response = await $fetch(
352
+ chatApi.path(`/conversations/${currentConversation.value.id}/messages`),
353
+ chatApi.withChatHeaders({ method: "POST", body })
354
+ );
355
+ acceptMessagePair(response);
356
+ }
235
357
  if (files && files.length > 0) {
236
358
  addFilesToConversation(
237
359
  currentConversation.value.id,
@@ -240,29 +362,46 @@ export function useChat() {
240
362
  "user"
241
363
  );
242
364
  }
243
- startPolling();
365
+ if (!acceptedByStream)
366
+ startPolling();
244
367
  return response.userMessage;
245
368
  } catch (err) {
246
369
  messages.value = messages.value.filter(
247
370
  (msg) => msg.id !== optimisticUserMessage.id && msg.id !== optimisticAssistantMessage.id
248
371
  );
372
+ streamingMessageId.value = null;
373
+ clearPreparingSteps();
374
+ directStream.disconnect();
249
375
  stopPolling();
376
+ if (err instanceof DOMException && err.name === "AbortError")
377
+ return null;
250
378
  const message = getChatApiErrorCode(err) === "workspace_agent_not_ready" ? WORKSPACE_AGENT_NOT_READY_TEXT : err instanceof Error ? err.message : "Erro ao enviar mensagem";
251
379
  error.value = message;
252
380
  Sentry.captureException(err);
253
381
  return null;
254
382
  } finally {
255
383
  sending.value = false;
384
+ gateHolding.value = false;
385
+ if (gateController === controller)
386
+ gateController = null;
256
387
  }
257
388
  };
258
389
  const clearConversation = () => {
259
390
  stopPolling();
391
+ gateController?.abort();
392
+ streamingMessageId.value = null;
393
+ clearPreparingSteps();
394
+ directStream.disconnect();
260
395
  currentConversation.value = null;
261
396
  messages.value = [];
262
397
  error.value = null;
263
398
  };
264
399
  const cancelling = ref(false);
265
400
  const cancelConversation = async () => {
401
+ if (gateHolding.value) {
402
+ gateController?.abort();
403
+ return true;
404
+ }
266
405
  if (!currentConversation.value || !hasPendingMessage.value)
267
406
  return false;
268
407
  cancelling.value = true;
@@ -278,6 +417,8 @@ export function useChat() {
278
417
  return false;
279
418
  }
280
419
  messages.value = markPendingAssistantMessagesInterrupted(messages.value);
420
+ streamingMessageId.value = null;
421
+ directStream.disconnect();
281
422
  stopPolling();
282
423
  return true;
283
424
  } catch (err) {
@@ -304,6 +445,8 @@ export function useChat() {
304
445
  messages: []
305
446
  };
306
447
  messages.value = [];
448
+ streamingMessageId.value = null;
449
+ directStream.disconnect();
307
450
  return true;
308
451
  } catch (err) {
309
452
  const message = err instanceof Error ? err.message : "Erro ao limpar conversa";
@@ -312,7 +455,7 @@ export function useChat() {
312
455
  return false;
313
456
  }
314
457
  };
315
- const retryMessage = async (messageId) => {
458
+ const retryMessage = async (messageId, wakeSessionId) => {
316
459
  if (!currentConversation.value)
317
460
  return false;
318
461
  error.value = null;
@@ -320,15 +463,22 @@ export function useChat() {
320
463
  if (!retry)
321
464
  return false;
322
465
  messages.value = retry.messages;
466
+ streamingMessageId.value = messageId;
467
+ directStream.beginMessage();
323
468
  try {
324
469
  await $fetch(
325
470
  chatApi.path(`/conversations/${currentConversation.value.id}/messages/${messageId}/retry`),
326
- chatApi.withChatHeaders({ method: "POST" })
471
+ chatApi.withChatHeaders({
472
+ method: "POST",
473
+ body: wakeSessionId ? { wakeSessionId } : {}
474
+ })
327
475
  );
328
476
  startPolling();
329
477
  return true;
330
478
  } catch (err) {
331
479
  messages.value = restoreAssistantMessageAfterRetryFailure(messages.value, retry.originalMessage);
480
+ streamingMessageId.value = null;
481
+ directStream.disconnect();
332
482
  const errorMessage = getChatApiErrorCode(err) === "workspace_agent_not_ready" ? WORKSPACE_AGENT_NOT_READY_TEXT : err instanceof Error ? err.message : "Erro ao reenviar mensagem";
333
483
  error.value = errorMessage;
334
484
  Sentry.captureException(err);
@@ -400,6 +550,8 @@ export function useChat() {
400
550
  };
401
551
  watch(hasPendingMessage, (isPending, wasPending) => {
402
552
  if (wasPending && !isPending) {
553
+ streamingMessageId.value = null;
554
+ directStream.disconnect();
403
555
  void generateTitle();
404
556
  }
405
557
  });
@@ -414,11 +566,14 @@ export function useChat() {
414
566
  error: readonly(error),
415
567
  isPolling: readonly(isPolling),
416
568
  hasPendingMessage,
569
+ streamingActiveMessageId,
570
+ preparingSteps: readonly(preparingSteps),
417
571
  cancelling: readonly(cancelling),
418
572
  generatingTitle: readonly(generatingTitle),
419
573
  animatedTitle: readonly(animatedTitle),
420
574
  loadConversation,
421
575
  setConversation,
576
+ adoptConversation,
422
577
  setConversationModel,
423
578
  pendingModelSelection,
424
579
  sendMessage,
@@ -0,0 +1,41 @@
1
+ import type { SendMessageResponse } from '../../types/schemas/chat/messages.js';
2
+ export type DirectStreamActivity = 'idle' | 'connected' | 'active';
3
+ export type SseFrame = {
4
+ event?: string;
5
+ data: string;
6
+ };
7
+ export type MessagePartsEvent = {
8
+ kind: 'message-parts';
9
+ runId: string;
10
+ deltaSeq: number;
11
+ parentToolUseId?: string | null;
12
+ event: Record<string, unknown>;
13
+ };
14
+ export type MessagePartsAccumulator = {
15
+ runId: string | null;
16
+ lastDeltaSeq: number;
17
+ textByIndex: Record<number, string>;
18
+ thinkingByIndex: Record<number, string>;
19
+ };
20
+ export declare function parseSseFrames(source: string): {
21
+ frames: SseFrame[];
22
+ rest: string;
23
+ };
24
+ export declare function createMessagePartsAccumulator(runId?: string | null): MessagePartsAccumulator;
25
+ export declare function applyMessagePartsEvent(accumulator: MessagePartsAccumulator, input: MessagePartsEvent): {
26
+ text: string;
27
+ thinking: string;
28
+ changed: boolean;
29
+ };
30
+ export declare function useDirectStream(options?: {
31
+ onResult?: () => void;
32
+ onDisconnect?: () => void;
33
+ }): {
34
+ activity: any;
35
+ streamingText: any;
36
+ streamingThinking: any;
37
+ activeRunId: any;
38
+ connectMessage: (url: string, init: RequestInit, onAccepted: (response: SendMessageResponse) => void) => Promise<SendMessageResponse>;
39
+ beginMessage: () => void;
40
+ disconnect: (reset?: boolean) => void;
41
+ };