@meistrari/chat-nuxt 3.9.2 → 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 (106) hide show
  1. package/README.md +50 -440
  2. package/dist/module.d.mts +1 -1
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/assets/css/markstream.css +1 -1
  5. package/dist/runtime/conversations/components/chat-conversation-creator-filter.vue +11 -67
  6. package/dist/runtime/conversations/components/chat-conversation-item.d.vue.ts +2 -1
  7. package/dist/runtime/conversations/components/chat-conversation-item.vue +29 -33
  8. package/dist/runtime/conversations/components/chat-conversation-item.vue.d.ts +2 -1
  9. package/dist/runtime/conversations/components/chat-conversation-list.d.vue.ts +0 -1
  10. package/dist/runtime/conversations/components/chat-conversation-list.vue +117 -158
  11. package/dist/runtime/conversations/components/chat-conversation-list.vue.d.ts +0 -1
  12. package/dist/runtime/conversations/components/chat-conversation-new-group-button.vue +34 -73
  13. package/dist/runtime/conversations/components/chat-conversation-search.vue +1 -21
  14. package/dist/runtime/conversations/components/chat-widget-history-list.d.vue.ts +12 -0
  15. package/dist/runtime/conversations/components/chat-widget-history-list.vue +27 -0
  16. package/dist/runtime/conversations/components/chat-widget-history-list.vue.d.ts +12 -0
  17. package/dist/runtime/conversations/composables/conversation-groups.d.ts +1 -0
  18. package/dist/runtime/conversations/composables/conversation-groups.js +2 -0
  19. package/dist/runtime/conversations/composables/conversation-item.js +4 -4
  20. package/dist/runtime/conversations/composables/conversation-list.js +14 -5
  21. package/dist/runtime/embed/components/chat-configuration-modal.d.vue.ts +3 -3
  22. package/dist/runtime/embed/components/chat-configuration-modal.vue.d.ts +3 -3
  23. package/dist/runtime/embed/components/chat-embed-inner.d.vue.ts +6 -3
  24. package/dist/runtime/embed/components/chat-embed-inner.vue +442 -174
  25. package/dist/runtime/embed/components/chat-embed-inner.vue.d.ts +6 -3
  26. package/dist/runtime/embed/components/chat-embed.d.vue.ts +31 -4
  27. package/dist/runtime/embed/components/chat-embed.vue +10 -4
  28. package/dist/runtime/embed/components/chat-embed.vue.d.ts +31 -4
  29. package/dist/runtime/embed/components/chat-topbar.vue +83 -85
  30. package/dist/runtime/embed/components/meistrari-chat-embed.d.vue.ts +5 -3
  31. package/dist/runtime/embed/components/meistrari-chat-embed.vue +118 -57
  32. package/dist/runtime/embed/components/meistrari-chat-embed.vue.d.ts +5 -3
  33. package/dist/runtime/embed/components/mobile/chat-mobile-drawer.d.vue.ts +0 -1
  34. package/dist/runtime/embed/components/mobile/chat-mobile-drawer.vue +2 -45
  35. package/dist/runtime/embed/components/mobile/chat-mobile-drawer.vue.d.ts +0 -1
  36. package/dist/runtime/embed/components/mobile/chat-mobile-home-suggestions.d.vue.ts +9 -5
  37. package/dist/runtime/embed/components/mobile/chat-mobile-home-suggestions.vue +14 -25
  38. package/dist/runtime/embed/components/mobile/chat-mobile-home-suggestions.vue.d.ts +9 -5
  39. package/dist/runtime/embed/components/mobile/chat-mobile-shell.d.vue.ts +4 -4
  40. package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue +2 -4
  41. package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue.d.ts +4 -4
  42. package/dist/runtime/embed/composables/use-agent-wake.d.ts +23 -0
  43. package/dist/runtime/embed/composables/use-agent-wake.js +182 -0
  44. package/dist/runtime/embed/feature-config.d.ts +7 -0
  45. package/dist/runtime/embed/feature-config.js +2 -1
  46. package/dist/runtime/embed/loading-messages.d.ts +16 -0
  47. package/dist/runtime/embed/loading-messages.js +10 -0
  48. package/dist/runtime/embed/types.d.ts +47 -8
  49. package/dist/runtime/files/icons.d.ts +1 -1
  50. package/dist/runtime/files/icons.js +1 -1
  51. package/dist/runtime/messages/components/chat-mention-dropdown.d.vue.ts +6 -1
  52. package/dist/runtime/messages/components/chat-mention-dropdown.vue +46 -22
  53. package/dist/runtime/messages/components/chat-mention-dropdown.vue.d.ts +6 -1
  54. package/dist/runtime/messages/components/chat-message-bubble.d.vue.ts +10 -1
  55. package/dist/runtime/messages/components/chat-message-bubble.vue +69 -76
  56. package/dist/runtime/messages/components/chat-message-bubble.vue.d.ts +10 -1
  57. package/dist/runtime/messages/components/chat-message-feedback.vue +25 -229
  58. package/dist/runtime/messages/components/chat-message-input.vue +176 -134
  59. package/dist/runtime/messages/components/chat-message-list.d.vue.ts +12 -1
  60. package/dist/runtime/messages/components/chat-message-list.vue +37 -24
  61. package/dist/runtime/messages/components/chat-message-list.vue.d.ts +12 -1
  62. package/dist/runtime/messages/components/chat-model-selector.vue +10 -22
  63. package/dist/runtime/messages/components/reasoning/chat-reasoning-message.vue +2 -3
  64. package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.d.vue.ts +2 -1
  65. package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue +69 -77
  66. package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue.d.ts +2 -1
  67. package/dist/runtime/messages/components/reasoning/chat-thinking-indicator.vue +3 -9
  68. package/dist/runtime/messages/components/widgets/chat-bash-widget.vue +3 -12
  69. package/dist/runtime/messages/components/widgets/chat-doc-sdk-widget.vue +3 -12
  70. package/dist/runtime/messages/components/widgets/chat-generic-tool-widget.vue +3 -12
  71. package/dist/runtime/messages/components/widgets/chat-grep-widget.vue +7 -17
  72. package/dist/runtime/messages/components/widgets/chat-read-widget.vue +7 -17
  73. package/dist/runtime/messages/components/widgets/chat-tela-skill-widget.vue +4 -13
  74. package/dist/runtime/messages/components/widgets/chat-thinking-widget.vue +11 -22
  75. package/dist/runtime/messages/components/widgets/chat-web-search-widget.vue +13 -40
  76. package/dist/runtime/messages/components/widgets/chat-workstation-query-widget.vue +52 -65
  77. package/dist/runtime/messages/components/widgets/chat-write-widget.vue +8 -18
  78. package/dist/runtime/messages/composables/chat.d.ts +8 -2
  79. package/dist/runtime/messages/composables/chat.js +180 -25
  80. package/dist/runtime/messages/composables/mention-autocomplete.js +4 -0
  81. package/dist/runtime/messages/composables/use-direct-stream.d.ts +41 -0
  82. package/dist/runtime/messages/composables/use-direct-stream.js +263 -0
  83. package/dist/runtime/messages/message-state.js +1 -0
  84. package/dist/runtime/messages/reasoning.d.ts +7 -0
  85. package/dist/runtime/messages/reasoning.js +5 -0
  86. package/dist/runtime/public/files/generic.svg +2 -5
  87. package/dist/runtime/public/files/image.svg +2 -1
  88. package/dist/runtime/types/embed.d.ts +1 -1
  89. package/dist/runtime/types/schemas/chat/conversations.d.ts +4 -0
  90. package/dist/runtime/types/schemas/chat/conversations.js +3 -0
  91. package/dist/runtime/types/schemas/chat/messages.d.ts +43 -0
  92. package/dist/runtime/types/schemas/chat/messages.js +31 -0
  93. package/dist/runtime/types/schemas/chat/models.d.ts +3 -2
  94. package/dist/runtime/types/schemas/chat/models.js +2 -1
  95. package/dist/runtime/types/schemas/chat/ui.d.ts +10 -0
  96. package/dist/runtime/types/schemas/chat/ui.js +1 -0
  97. package/dist/runtime/types/schemas/integrations/tela-agent-api.d.ts +35 -0
  98. package/dist/runtime/types/schemas/integrations/tela-agent-api.js +17 -0
  99. package/dist/types.d.mts +1 -1
  100. package/package.json +5 -4
  101. package/dist/runtime/conversations/components/chat-conversation-new-group-popover-content.d.vue.ts +0 -23
  102. package/dist/runtime/conversations/components/chat-conversation-new-group-popover-content.vue +0 -75
  103. package/dist/runtime/conversations/components/chat-conversation-new-group-popover-content.vue.d.ts +0 -23
  104. package/dist/runtime/messages/components/reasoning/chat-reasoning-step-item.d.vue.ts +0 -37
  105. package/dist/runtime/messages/components/reasoning/chat-reasoning-step-item.vue +0 -125
  106. package/dist/runtime/messages/components/reasoning/chat-reasoning-step-item.vue.d.ts +0 -37
@@ -335,86 +335,73 @@ const displayName = computed(() => {
335
335
  </script>
336
336
 
337
337
  <template>
338
- <div class="bg-subtle rounded-16px border border-gray-200">
339
- <!-- Header -->
340
- <div class="px-16px py-16px flex items-center justify-between">
341
- <div class="flex-1 min-w-0">
342
- <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary truncate">
343
- Consulta de Dados
344
- </h3>
345
- <p v-if="displayName !== 'Workstation'" class="text-12px text-secondary truncate mt-2px">
346
- {{ displayName }}
347
- </p>
348
- </div>
349
- <TelaStatus
350
- :variant="statusVariant"
351
- :label="statusLabel"
352
- :is-icon-animated="!toolResult"
353
- />
354
- </div>
338
+ <TelaChatWidgetCard
339
+ title="Consulta de Dados"
340
+ :status-variant="statusVariant"
341
+ :status-label="statusLabel ?? ''"
342
+ :is-icon-animated="!toolResult"
343
+ >
344
+ <template v-if="displayName !== 'Workstation'" #subtitle>
345
+ {{ displayName }}
346
+ </template>
355
347
 
356
348
  <!-- Filters preview (when collapsed) -->
357
349
  <div
358
350
  v-if="!expanded && canExpand && (formattedFilters || formattedOutputQuery)"
359
- class="px-16px pb-12px"
351
+ class="flex flex-wrap items-center gap-6px"
360
352
  >
361
- <div class="flex flex-wrap items-center gap-6px">
362
- <template v-if="formattedFilters">
363
- <span
364
- v-for="filter in formattedFilters.slice(0, 3)"
365
- :key="filter"
366
- class="text-11px px-8px py-3px rounded-6px bg-gray-100 text-gray-600"
367
- >
368
- {{ filter }}
369
- </span>
370
- </template>
371
- <template v-if="formattedOutputQuery">
372
- <span
373
- v-for="query in formattedOutputQuery.slice(0, 3)"
374
- :key="query"
375
- class="text-11px px-8px py-3px rounded-6px bg-blue-100 text-blue-700"
376
- >
377
- {{ query }}
378
- </span>
379
- </template>
353
+ <template v-if="formattedFilters">
380
354
  <span
381
- v-if="(formattedFilters?.length || 0) + (formattedOutputQuery?.length || 0) > 3"
382
- class="text-11px text-gray-400"
355
+ v-for="filter in formattedFilters.slice(0, 3)"
356
+ :key="filter"
357
+ class="text-11px px-8px py-3px rounded-6px bg-gray-100 text-gray-600"
383
358
  >
384
- +{{ (formattedFilters?.length || 0) + (formattedOutputQuery?.length || 0) - 3 }} mais
359
+ {{ filter }}
385
360
  </span>
386
- </div>
361
+ </template>
362
+ <template v-if="formattedOutputQuery">
363
+ <span
364
+ v-for="query in formattedOutputQuery.slice(0, 3)"
365
+ :key="query"
366
+ class="text-11px px-8px py-3px rounded-6px bg-blue-100 text-blue-700"
367
+ >
368
+ {{ query }}
369
+ </span>
370
+ </template>
371
+ <span
372
+ v-if="(formattedFilters?.length || 0) + (formattedOutputQuery?.length || 0) > 3"
373
+ class="text-11px text-gray-400"
374
+ >
375
+ +{{ (formattedFilters?.length || 0) + (formattedOutputQuery?.length || 0) - 3 }} mais
376
+ </span>
387
377
  </div>
388
378
 
389
379
  <!-- Launch message (when only have launching output) -->
390
- <div v-if="toolResult && isLaunchMessage" class="px-16px pb-16px">
391
- <div class="bg-blue-50 rounded-xl border-0.5px border-blue-200 px-16px py-12px">
392
- <div class="flex items-center gap-8px">
393
- <div class="i-ph-clock text-16px text-blue-600 animate-pulse" />
394
- <p class="text-13px text-blue-700 m-0">
395
- Aguardando resultados da consulta...
396
- </p>
397
- </div>
380
+ <div v-if="toolResult && isLaunchMessage" class="bg-blue-50 rounded-xl border-0.5px border-blue-200 px-16px py-12px">
381
+ <div class="flex items-center gap-8px">
382
+ <div class="i-ph-clock text-16px text-blue-600 animate-pulse" />
383
+ <p class="text-13px text-blue-700 m-0">
384
+ Aguardando resultados da consulta...
385
+ </p>
398
386
  </div>
399
387
  </div>
400
388
 
401
389
  <!-- Expand/Collapse button -->
402
- <div v-if="toolResult && !isLaunchMessage" class="px-16px pb-12px">
403
- <button
404
- type="button"
405
- class="w-full flex items-center justify-center gap-6px px-12px py-8px text-13px font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-8px transition-colors"
406
- @click="expanded = !expanded"
407
- >
408
- <span>{{ expanded ? "Ocultar detalhes" : "Ver detalhes" }}</span>
409
- <div
410
- class="i-ph-caret-down text-12px transition-transform"
411
- :class="expanded && 'rotate-180'"
412
- />
413
- </button>
414
- </div>
390
+ <button
391
+ v-if="toolResult && !isLaunchMessage"
392
+ type="button"
393
+ class="w-full flex items-center justify-center gap-6px px-12px py-8px text-13px font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-8px transition-colors"
394
+ @click="expanded = !expanded"
395
+ >
396
+ <span>{{ expanded ? "Ocultar detalhes" : "Ver detalhes" }}</span>
397
+ <div
398
+ class="i-ph-caret-down text-12px transition-transform"
399
+ :class="expanded && 'rotate-180'"
400
+ />
401
+ </button>
415
402
 
416
403
  <!-- Expanded content -->
417
- <div v-if="expanded && toolResult && !isLaunchMessage" class="px-16px pb-16px space-y-16px">
404
+ <template v-if="expanded && toolResult && !isLaunchMessage">
418
405
  <!-- Filters section -->
419
406
  <div v-if="formattedFilters || formattedOutputQuery">
420
407
  <div class="text-14px font-580 leading-16px tracking-[-0.15px] text-gray-700 mb-2">
@@ -548,6 +535,6 @@ const displayName = computed(() => {
548
535
  <pre class="text-14px text-primary whitespace-pre-wrap break-words m-0">{{ toolResult.output }}</pre>
549
536
  </div>
550
537
  </div>
551
- </div>
552
- </div>
538
+ </template>
539
+ </TelaChatWidgetCard>
553
540
  </template>
@@ -90,23 +90,13 @@ const hasMoreLines = computed(() => {
90
90
  </script>
91
91
 
92
92
  <template>
93
- <div class="bg-subtle rounded-16px border border-gray-200">
94
- <!-- Header -->
95
- <div class="px-16px py-16px flex items-center justify-between">
96
- <div class="flex-1 min-w-0">
97
- <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary truncate">
98
- Write
99
- </h3>
100
- <p class="text-12px text-secondary truncate mt-2px">
101
- {{ fileName }}
102
- <span v-if="parentDir" class="opacity-60">em {{ parentDir }}</span>
103
- </p>
104
- </div>
105
- <TelaStatus :variant="statusVariant" :label="statusLabel" />
106
- </div>
93
+ <TelaChatWidgetCard title="Write" :status-variant="statusVariant" :status-label="statusLabel">
94
+ <template #subtitle>
95
+ {{ fileName }}
96
+ <span v-if="parentDir" class="opacity-60">em {{ parentDir }}</span>
97
+ </template>
107
98
 
108
- <!-- Content -->
109
- <div v-if="toolResult" class="px-16px pb-16px space-y-16px">
99
+ <template v-if="toolResult">
110
100
  <!-- File path section -->
111
101
  <div>
112
102
  <div class="text-14px font-580 leading-16px tracking-[-0.15px] text-gray-700 mb-2">
@@ -156,8 +146,8 @@ const hasMoreLines = computed(() => {
156
146
  </p>
157
147
  </div>
158
148
  </div>
159
- </div>
160
- </div>
149
+ </template>
150
+ </TelaChatWidgetCard>
161
151
  </template>
162
152
 
163
153
  <style scoped>
@@ -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,
@@ -27,6 +27,7 @@ export function useMentionAutocomplete(options) {
27
27
  const query = ref("");
28
28
  const highlightedIndex = ref(-1);
29
29
  const lastQuery = ref("");
30
+ const triggerCaretRect = ref(null);
30
31
  let triggerTextOffset = -1;
31
32
  const isOpen = computed(() => activeTrigger.value !== null);
32
33
  const filteredItems = computed(() => {
@@ -89,10 +90,12 @@ export function useMentionAutocomplete(options) {
89
90
  const mentionTrigger = findMentionTrigger(getTextBeforeCursor());
90
91
  if (!mentionTrigger) {
91
92
  activeTrigger.value = null;
93
+ triggerCaretRect.value = null;
92
94
  return;
93
95
  }
94
96
  activeTrigger.value = mentionTrigger.trigger;
95
97
  triggerTextOffset = mentionTrigger.offset;
98
+ triggerCaretRect.value = getCaretRangeAtOffset(triggerTextOffset)?.getBoundingClientRect() ?? null;
96
99
  if (mentionTrigger.query !== lastQuery.value) {
97
100
  highlightedIndex.value = -1;
98
101
  lastQuery.value = mentionTrigger.query;
@@ -314,6 +317,7 @@ ${varLines}
314
317
  isOpen,
315
318
  activeTrigger: readonly(activeTrigger),
316
319
  query: readonly(query),
320
+ triggerCaretRect: readonly(triggerCaretRect),
317
321
  filteredItems,
318
322
  highlightedIndex,
319
323
  handleKeydown,
@@ -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
+ };