@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
package/README.md CHANGED
@@ -59,7 +59,7 @@ Declared in `src/runtime/embed/types.ts`; prop, feature-flag, and event payload
59
59
  | `telaAgentInputs` | `TelaAgentExecutionInput[] \| null` | `undefined` | Inputs sent with each Tela agent run; hides the variables panel when non-null |
60
60
  | `workspaceSettings` | `WorkspaceSettings \| null` | `null` | Override settings from host app; not accepted in Tela agent mode |
61
61
  | `user` | `ChatActor \| null` | `null` | Override current user display info; not accepted in Tela agent mode |
62
- | `features` | `Partial<ChatFeatureConfig>` | `{}` | Toggle optional features: `showUsageTab`, `showDebugOption`, `showCancelButton` (all `false`) |
62
+ | `features` | `Partial<ChatFeatureConfig>` | `{}` | Toggle optional features: `showUsageTab`, `showDebugOption`, `showCancelButton` (default `false`) and `showTtft` (default `true`) |
63
63
  | `loadingMessages` | `readonly string[] \| null` | Default chat messages | Override pending-response messages |
64
64
  | `loadingMessagesMode` | `'ordered' \| 'random' \| null` | `'ordered'` | Rotate custom messages in order or at random; only applies when `loadingMessages` has a non-empty message |
65
65
  | `customComponents` | `Record<string, Component>` | `undefined` | Custom markdown renderers keyed by markdown node or tag name |
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "3.10.0",
4
+ "version": "3.11.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -18,6 +18,7 @@ import { useConversationFiles } from "../../files/composables/conversation-files
18
18
  import { useConversationFilesPanel } from "../../files/composables/conversation-files-panel";
19
19
  import { useConversations } from "../../conversations/composables/conversations";
20
20
  import { useEmbedConfig } from "../composables/embed-config";
21
+ import { useAgentWake } from "../composables/use-agent-wake";
21
22
  import { useFileDrop } from "../../files/composables/file-drop";
22
23
  import { useFilePreviewPanel } from "../../files/composables/file-preview-panel";
23
24
  import { usePendingConversationGroup } from "../../conversations/composables/pending-conversation-group";
@@ -155,16 +156,26 @@ const {
155
156
  generatingTitle,
156
157
  animatedTitle,
157
158
  hasPendingMessage,
159
+ streamingActiveMessageId,
160
+ preparingSteps,
158
161
  loadConversation,
159
162
  setConversation,
163
+ adoptConversation,
160
164
  setConversationModel,
161
165
  pendingModelSelection,
162
166
  sendMessage,
163
167
  retryMessage,
164
168
  cancelConversation,
165
169
  startPolling,
166
- clearConversation
170
+ clearConversation,
171
+ error: chatError
167
172
  } = useChat();
173
+ const agentWake = useAgentWake();
174
+ const modelUpdateInFlight = ref(null);
175
+ watch(chatError, (message) => {
176
+ if (message)
177
+ statusToast.update({ text: message, icon: "i-ph-warning" });
178
+ });
168
179
  const conversationIdRef = computed(() => currentConversation.value?.id ?? null);
169
180
  const { total, files: conversationFiles } = useConversationFiles(conversationIdRef);
170
181
  const selectedModel = computed({
@@ -175,14 +186,24 @@ const selectedModel = computed({
175
186
  if (conversation && conversation.model !== value) {
176
187
  const previousModel = conversation.model;
177
188
  setConversationModel(value);
178
- void updateConversationModel(conversation.id, value).then((updated) => {
189
+ const update = updateConversationModel(conversation.id, value).then((updated) => {
179
190
  if (!updated && currentConversation.value?.id === conversation.id && currentConversation.value.model === value) {
180
191
  setConversationModel(previousModel);
181
192
  }
193
+ return updated;
194
+ });
195
+ modelUpdateInFlight.value = update.finally(() => {
196
+ if (modelUpdateInFlight.value === update)
197
+ modelUpdateInFlight.value = null;
182
198
  });
183
199
  }
184
200
  }
185
201
  });
202
+ watch(selectedModel, (model, previousModel) => {
203
+ if (!isReady.value || model === previousModel || currentConversation.value?.threadSessionId)
204
+ return;
205
+ void agentWake.wake(model);
206
+ });
186
207
  const filesCount = computed(() => total.value);
187
208
  const hasMessages = computed(() => messages.value.length > 0);
188
209
  const mobilePreviewFile = computed(() => {
@@ -222,6 +243,10 @@ function updateConversationId(nextConversationId) {
222
243
  }
223
244
  async function ensureConversationLoaded(conversationId) {
224
245
  if (currentConversation.value?.id === conversationId) {
246
+ void agentWake.wake(
247
+ currentConversation.value.model,
248
+ currentConversation.value.threadSessionId ?? void 0
249
+ );
225
250
  startPolling();
226
251
  return;
227
252
  }
@@ -232,12 +257,17 @@ async function ensureConversationLoaded(conversationId) {
232
257
  try {
233
258
  await loadConversation(conversationId);
234
259
  if (currentConversation.value?.id !== conversationId) {
260
+ agentWake.skip();
235
261
  statusToast.update({
236
262
  text: "Erro ao carregar conversa",
237
263
  icon: "i-ph-warning"
238
264
  });
239
265
  return;
240
266
  }
267
+ void agentWake.wake(
268
+ currentConversation.value.model,
269
+ currentConversation.value.threadSessionId ?? void 0
270
+ );
241
271
  startPolling();
242
272
  } finally {
243
273
  if (loadingConversationId.value === conversationId) {
@@ -247,6 +277,7 @@ async function ensureConversationLoaded(conversationId) {
247
277
  }
248
278
  function resetConversation(shouldEmit = true) {
249
279
  clearConversation();
280
+ void agentWake.wake(selectedModel.value);
250
281
  activeTab.value = "chat";
251
282
  closeFilesPanel();
252
283
  closePreview();
@@ -404,43 +435,55 @@ const conversationCreator = computed(
404
435
  getMember
405
436
  })
406
437
  );
438
+ function claimableWakeSessionId() {
439
+ return agentWake.state.value === "ready" && !currentConversation.value?.threadSessionId ? agentWake.sessionId.value : void 0;
440
+ }
407
441
  async function handleSend(content, files, agentInputs) {
408
- let createdConversationId = null;
409
- const resolvedAgentInputs = resolveTelaAgentInputs(
410
- agentInputs,
411
- embedConfig?.telaAgentInputs?.value
412
- );
413
- if (!activeConversationId.value || !currentConversation.value) {
414
- const conversation = await createConversation({
415
- model: selectedModel.value,
416
- ...resolvedAgentInputs !== void 0 ? { agentInputs: resolvedAgentInputs } : {}
417
- });
442
+ const resolvedAgentInputs = resolveTelaAgentInputs(agentInputs, embedConfig?.telaAgentInputs?.value);
443
+ let claimedWakeSessionId;
444
+ const sent = await sendMessage(content, files.length > 0 ? files : void 0, resolvedAgentInputs, async (signal) => {
445
+ if (!activeConversationId.value || !currentConversation.value) {
446
+ const conversation2 = await createConversation({
447
+ model: selectedModel.value,
448
+ ...resolvedAgentInputs !== void 0 ? { agentInputs: resolvedAgentInputs } : {}
449
+ });
450
+ if (!conversation2)
451
+ throw new Error("Erro ao criar conversa");
452
+ const groupId = pendingConversationGroupId.value;
453
+ clearPendingConversationGroup();
454
+ const grouped = groupId && await moveConversationToGroup(conversation2.id, groupId);
455
+ adoptConversation(grouped ? { ...conversation2, groupId } : conversation2);
456
+ updateConversationId(conversation2.id);
457
+ }
458
+ if (modelUpdateInFlight.value)
459
+ await modelUpdateInFlight.value;
460
+ const conversation = currentConversation.value;
418
461
  if (!conversation)
419
- return;
420
- const groupId = pendingConversationGroupId.value;
421
- clearPendingConversationGroup();
422
- if (groupId && await moveConversationToGroup(conversation.id, groupId)) {
423
- const groupedConversation = { ...conversation, groupId };
424
- setConversation(groupedConversation);
462
+ throw new Error("Erro ao preparar conversa");
463
+ if (conversation.threadSessionId) {
464
+ await agentWake.wake(
465
+ conversation.model,
466
+ conversation.threadSessionId
467
+ );
425
468
  } else {
426
- setConversation(conversation);
469
+ await agentWake.waitUntilSettled(signal);
427
470
  }
428
- createdConversationId = conversation.id;
429
- }
430
- if (!currentConversation.value)
431
- return;
432
- const sendPromise = sendMessage(
433
- content,
434
- files.length > 0 ? files : void 0,
435
- resolvedAgentInputs
436
- );
437
- if (createdConversationId) {
438
- updateConversationId(createdConversationId);
439
- }
440
- await sendPromise;
471
+ claimedWakeSessionId = claimableWakeSessionId();
472
+ return claimedWakeSessionId ? { wakeSessionId: claimedWakeSessionId } : {};
473
+ });
474
+ if (sent && claimedWakeSessionId)
475
+ agentWake.consume();
441
476
  }
442
477
  async function handleRetry(messageId) {
443
- await retryMessage(messageId);
478
+ if (currentConversation.value?.threadSessionId) {
479
+ await agentWake.wake(
480
+ currentConversation.value.model,
481
+ currentConversation.value.threadSessionId
482
+ );
483
+ } else {
484
+ await agentWake.waitUntilSettled();
485
+ }
486
+ await retryMessage(messageId, claimableWakeSessionId());
444
487
  }
445
488
  async function handleNewConversation(groupId) {
446
489
  setPendingConversationGroup(groupId);
@@ -765,7 +808,14 @@ async function handleDelete() {
765
808
  @tab-change="handleTabChange"
766
809
  >
767
810
  <template #loading>
768
- <ChatMessageList :messages="[]" loading variant="mobile" />
811
+ <ChatMessageList
812
+ :messages="[]"
813
+ :streaming-active-message-id="streamingActiveMessageId"
814
+ :preparing-steps="preparingSteps"
815
+ :show-ttft="resolvedFeatures.showTtft"
816
+ loading
817
+ variant="mobile"
818
+ />
769
819
  <ChatMessageInput
770
820
  v-model:selected-model="selectedModel"
771
821
  :disabled="true"
@@ -798,7 +848,6 @@ async function handleDelete() {
798
848
  <ChatMessageInput
799
849
  ref="messageInputRef"
800
850
  v-model:selected-model="selectedModel"
801
- :disabled="hasPendingMessage"
802
851
  :loading="hasPendingMessage"
803
852
  :creator-name="currentUser?.name ?? void 0"
804
853
  :creator-email="
@@ -823,13 +872,15 @@ async function handleDelete() {
823
872
 
824
873
  <ChatMessageList
825
874
  :messages="messages"
875
+ :streaming-active-message-id="streamingActiveMessageId"
876
+ :preparing-steps="preparingSteps"
877
+ :show-ttft="resolvedFeatures.showTtft"
826
878
  variant="mobile"
827
879
  @retry="handleRetry"
828
880
  />
829
881
  <ChatMessageInput
830
882
  ref="messageInputRef"
831
883
  v-model:selected-model="selectedModel"
832
- :disabled="hasPendingMessage"
833
884
  :loading="hasPendingMessage"
834
885
  :creator-name="currentUser?.name ?? void 0"
835
886
  :creator-email="
@@ -950,6 +1001,9 @@ async function handleDelete() {
950
1001
  >
951
1002
  <ChatMessageList
952
1003
  :messages="[]"
1004
+ :streaming-active-message-id="streamingActiveMessageId"
1005
+ :preparing-steps="preparingSteps"
1006
+ :show-ttft="resolvedFeatures.showTtft"
953
1007
  loading
954
1008
  :variant="compact ? 'widget' : 'page'"
955
1009
  />
@@ -991,7 +1045,6 @@ async function handleDelete() {
991
1045
  <ChatMessageInput
992
1046
  ref="messageInputRef"
993
1047
  v-model:selected-model="selectedModel"
994
- :disabled="hasPendingMessage"
995
1048
  :loading="hasPendingMessage"
996
1049
  :creator-name="currentUser?.name ?? void 0"
997
1050
  :creator-email="
@@ -1019,13 +1072,15 @@ async function handleDelete() {
1019
1072
  >
1020
1073
  <ChatMessageList
1021
1074
  :messages="messages"
1075
+ :streaming-active-message-id="streamingActiveMessageId"
1076
+ :preparing-steps="preparingSteps"
1077
+ :show-ttft="resolvedFeatures.showTtft"
1022
1078
  :variant="compact ? 'widget' : 'page'"
1023
1079
  @retry="handleRetry"
1024
1080
  />
1025
1081
  <ChatMessageInput
1026
1082
  ref="messageInputRef"
1027
1083
  v-model:selected-model="selectedModel"
1028
- :disabled="hasPendingMessage"
1029
1084
  :loading="hasPendingMessage"
1030
1085
  :creator-name="currentUser?.name ?? void 0"
1031
1086
  :creator-email="
@@ -225,7 +225,6 @@ function startNewConversationFromOutdatedSettings() {
225
225
  <ChatMessageInput
226
226
  ref="homeInputRef"
227
227
  v-model:selected-model="selectedModel"
228
- :disabled="hasPendingMessage"
229
228
  :loading="hasPendingMessage"
230
229
  :creator-name="currentUser?.name ?? void 0"
231
230
  :creator-email="currentUser?.email ?? void 0"
@@ -0,0 +1,23 @@
1
+ import type { TelaAgentWakeResponse } from '../../types/schemas/integrations/tela-agent-api.js';
2
+ export type AgentWakeSnapshot = {
3
+ state: TelaAgentWakeResponse['status'] | 'idle';
4
+ sessionId?: string;
5
+ model?: string;
6
+ };
7
+ export declare function pollAgentWake(options: {
8
+ request: () => Promise<TelaAgentWakeResponse>;
9
+ onResponse?: (response: TelaAgentWakeResponse) => void;
10
+ signal?: AbortSignal;
11
+ pollIntervalMs?: number;
12
+ budgetMs?: number;
13
+ now?: () => number;
14
+ wait?: (delayMs: number, signal?: AbortSignal) => Promise<void>;
15
+ }): Promise<TelaAgentWakeResponse>;
16
+ export declare function useAgentWake(): {
17
+ state: any;
18
+ sessionId: any;
19
+ wake: (model?: string | null, requestedSessionId?: string) => Promise<TelaAgentWakeResponse>;
20
+ skip: () => void;
21
+ consume: () => void;
22
+ waitUntilSettled: (signal?: AbortSignal) => Promise<AgentWakeSnapshot>;
23
+ };
@@ -0,0 +1,182 @@
1
+ import { useChatApi } from "../../shared/composables/chat-api.js";
2
+ const WAKE_POLL_INTERVAL_MS = 1500;
3
+ const WAKE_BUDGET_MS = 3e4;
4
+ const WAKE_SETTLE_TIMEOUT_MS = 35e3;
5
+ export async function pollAgentWake(options) {
6
+ const now = options.now ?? Date.now;
7
+ const deadline = now() + (options.budgetMs ?? WAKE_BUDGET_MS);
8
+ const wait = options.wait ?? ((delayMs, signal) => new Promise((resolve, reject) => {
9
+ let timer;
10
+ const onAbort = () => {
11
+ if (timer)
12
+ clearTimeout(timer);
13
+ reject(new DOMException("Aborted", "AbortError"));
14
+ };
15
+ timer = setTimeout(() => {
16
+ signal?.removeEventListener("abort", onAbort);
17
+ resolve();
18
+ }, delayMs);
19
+ if (signal?.aborted)
20
+ onAbort();
21
+ else
22
+ signal?.addEventListener("abort", onAbort, { once: true });
23
+ }));
24
+ try {
25
+ while (!options.signal?.aborted && now() <= deadline) {
26
+ const response = await options.request();
27
+ options.onResponse?.(response);
28
+ if (response.status !== "waking")
29
+ return response;
30
+ const remaining = deadline - now();
31
+ if (remaining <= 0)
32
+ break;
33
+ await wait(Math.min(options.pollIntervalMs ?? WAKE_POLL_INTERVAL_MS, remaining), options.signal);
34
+ }
35
+ } catch (error) {
36
+ if (error instanceof DOMException && error.name === "AbortError")
37
+ throw error;
38
+ return { status: "unavailable" };
39
+ }
40
+ return { status: "unavailable" };
41
+ }
42
+ export function useAgentWake() {
43
+ const chatApi = useChatApi();
44
+ const snapshot = ref({ state: "idle" });
45
+ const state = computed(() => snapshot.value.state);
46
+ const sessionId = computed(() => snapshot.value.sessionId);
47
+ let generation = 0;
48
+ let abortController = null;
49
+ let activeWake = null;
50
+ function setSnapshot(next) {
51
+ snapshot.value = next;
52
+ }
53
+ function skip() {
54
+ generation++;
55
+ abortController?.abort();
56
+ abortController = null;
57
+ activeWake = null;
58
+ setSnapshot({
59
+ state: "idle",
60
+ ...snapshot.value.sessionId ? { sessionId: snapshot.value.sessionId } : {}
61
+ });
62
+ }
63
+ function consume() {
64
+ generation++;
65
+ abortController?.abort();
66
+ abortController = null;
67
+ activeWake = null;
68
+ setSnapshot({ state: "idle" });
69
+ }
70
+ async function wake(model, requestedSessionId) {
71
+ generation++;
72
+ const currentGeneration = generation;
73
+ abortController?.abort();
74
+ const controller = new AbortController();
75
+ abortController = controller;
76
+ let wakeSessionId = requestedSessionId ?? snapshot.value.sessionId;
77
+ setSnapshot({
78
+ state: "waking",
79
+ ...wakeSessionId ? { sessionId: wakeSessionId } : {},
80
+ ...model ? { model } : {}
81
+ });
82
+ if (import.meta.server) {
83
+ const unavailable = { status: "unavailable" };
84
+ setSnapshot({ state: unavailable.status });
85
+ return unavailable;
86
+ }
87
+ const run = async () => {
88
+ try {
89
+ const result = await pollAgentWake({
90
+ signal: controller.signal,
91
+ request: () => $fetch(
92
+ chatApi.path("/agent/wake"),
93
+ chatApi.withChatHeaders({
94
+ method: "POST",
95
+ signal: controller.signal,
96
+ body: {
97
+ ...wakeSessionId ? { sessionId: wakeSessionId } : {},
98
+ ...model ? { model } : {}
99
+ }
100
+ })
101
+ ),
102
+ onResponse: (response) => {
103
+ if (currentGeneration !== generation)
104
+ return;
105
+ wakeSessionId = response.sessionId ?? wakeSessionId;
106
+ setSnapshot({
107
+ state: response.status,
108
+ ...wakeSessionId ? { sessionId: wakeSessionId } : {},
109
+ ...model ? { model } : {}
110
+ });
111
+ }
112
+ });
113
+ if (currentGeneration === generation) {
114
+ setSnapshot({
115
+ state: result.status,
116
+ ...result.sessionId ? { sessionId: result.sessionId } : {},
117
+ ...model ? { model } : {}
118
+ });
119
+ }
120
+ return result;
121
+ } catch (error) {
122
+ if (!(error instanceof DOMException && error.name === "AbortError") && currentGeneration === generation)
123
+ setSnapshot({ state: "unavailable" });
124
+ return { status: "unavailable" };
125
+ } finally {
126
+ if (abortController === controller)
127
+ abortController = null;
128
+ }
129
+ };
130
+ const pending = run();
131
+ activeWake = pending;
132
+ try {
133
+ return await pending;
134
+ } finally {
135
+ if (activeWake === pending)
136
+ activeWake = null;
137
+ }
138
+ }
139
+ async function waitUntilSettled(signal) {
140
+ if (snapshot.value.state !== "waking" || !activeWake)
141
+ return snapshot.value;
142
+ const pending = activeWake;
143
+ await new Promise((resolve, reject) => {
144
+ let settled = false;
145
+ let timer;
146
+ let onAbort = () => {
147
+ };
148
+ const finish = (error) => {
149
+ if (settled)
150
+ return;
151
+ settled = true;
152
+ if (timer)
153
+ clearTimeout(timer);
154
+ signal?.removeEventListener("abort", onAbort);
155
+ if (error)
156
+ reject(error);
157
+ else
158
+ resolve();
159
+ };
160
+ onAbort = () => finish(new DOMException("Aborted", "AbortError"));
161
+ timer = setTimeout(() => finish(), WAKE_SETTLE_TIMEOUT_MS);
162
+ if (signal?.aborted)
163
+ onAbort();
164
+ else
165
+ signal?.addEventListener("abort", onAbort, { once: true });
166
+ pending.then(() => finish(), () => finish());
167
+ });
168
+ return snapshot.value;
169
+ }
170
+ onUnmounted(() => {
171
+ generation++;
172
+ abortController?.abort();
173
+ });
174
+ return {
175
+ state,
176
+ sessionId,
177
+ wake,
178
+ skip,
179
+ consume,
180
+ waitUntilSettled
181
+ };
182
+ }
@@ -1,7 +1,4 @@
1
- /**
2
- * Optional UI toggles the host passes per embed instance. All default to `false`;
3
- * the host owns the policy (hardcoded, role-gated, or from a feature-flag provider).
4
- */
1
+ /** Optional UI toggles the host passes per embed instance. */
5
2
  export type ChatFeatureConfig = {
6
3
  /** Usage tab in the chat topbar, with per-conversation token and cost stats. */
7
4
  showUsageTab?: boolean;
@@ -9,10 +6,13 @@ export type ChatFeatureConfig = {
9
6
  showDebugOption?: boolean;
10
7
  /** "Stop generation" button while a response streams. */
11
8
  showCancelButton?: boolean;
9
+ /** Time to first token shown in assistant message metadata. Defaults to true. */
10
+ showTtft?: boolean;
12
11
  };
13
12
  export type ResolvedChatFeatureConfig = {
14
13
  showUsageTab: boolean;
15
14
  showDebugOption: boolean;
16
15
  showCancelButton: boolean;
16
+ showTtft: boolean;
17
17
  };
18
18
  export declare function resolveChatFeatures(features?: Partial<ChatFeatureConfig>): ResolvedChatFeatureConfig;
@@ -2,6 +2,7 @@ export function resolveChatFeatures(features) {
2
2
  return {
3
3
  showUsageTab: features?.showUsageTab ?? false,
4
4
  showDebugOption: features?.showDebugOption ?? false,
5
- showCancelButton: features?.showCancelButton ?? false
5
+ showCancelButton: features?.showCancelButton ?? false,
6
+ showTtft: features?.showTtft ?? true
6
7
  };
7
8
  }
@@ -1,4 +1,20 @@
1
1
  export declare const DEFAULT_CHAT_LOADING_MESSAGES: readonly ["Analisando sua pergunta", "Processando informações", "Consultando base de conhecimento", "Organizando ideias", "Elaborando resposta", "Refinando detalhes", "Verificando contexto", "Conectando conceitos", "Preparando resultado", "Quase lá", "Finalizando análise", "Estruturando resposta"];
2
+ /**
3
+ * Stand-in steps for a turn that has not produced its own reasoning yet. They
4
+ * accumulate, so the bubble reads as an agent working through the wait rather
5
+ * than as a loading placeholder. The user message is already delivered at this
6
+ * point, so the wait belongs to the answer.
7
+ */
8
+ export declare const CHAT_PREPARING_STEPS: readonly ["Preparando o ambiente de trabalho", "Analisando a mensagem"];
9
+ /**
10
+ * The first delay is measured from the send, every later one from the step
11
+ * before it — so the sequence paces itself instead of racing a shared clock.
12
+ * Late enough that a turn answering promptly never shows a stand-in, and
13
+ * jittered so it does not look like a fixed-timer artifact.
14
+ */
15
+ export declare const CHAT_PREPARING_STEP_DELAYS_MS: readonly [2500, 4000];
16
+ export declare const CHAT_PREPARING_STEP_JITTER_MS = 500;
17
+ export declare function nextChatPreparingStepDelay(stepIndex: number, random?: () => number): number;
2
18
  export type ChatLoadingMessageMode = 'ordered' | 'random';
3
19
  export type ChatLoadingMessagesInput = {
4
20
  loadingMessages?: readonly string[] | null;
@@ -12,6 +12,16 @@ export const DEFAULT_CHAT_LOADING_MESSAGES = [
12
12
  "Finalizando an\xE1lise",
13
13
  "Estruturando resposta"
14
14
  ];
15
+ export const CHAT_PREPARING_STEPS = [
16
+ "Preparando o ambiente de trabalho",
17
+ "Analisando a mensagem"
18
+ ];
19
+ export const CHAT_PREPARING_STEP_DELAYS_MS = [2500, 4e3];
20
+ export const CHAT_PREPARING_STEP_JITTER_MS = 500;
21
+ export function nextChatPreparingStepDelay(stepIndex, random = Math.random) {
22
+ const base = CHAT_PREPARING_STEP_DELAYS_MS[stepIndex] ?? CHAT_PREPARING_STEP_DELAYS_MS.at(-1);
23
+ return base + Math.floor(random() * CHAT_PREPARING_STEP_JITTER_MS);
24
+ }
15
25
  function normalizeMessages(messages) {
16
26
  if (!messages?.length)
17
27
  return [];
@@ -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;
@@ -6,7 +6,10 @@ import { isPromptTooLongFailure } from "../../types/schemas/chat/messages.js";
6
6
  import { parseContentWithFiles } from "../../files/content-detection";
7
7
  import ChatVaultLink from "../../files/components/chat-vault-link.vue";
8
8
  const props = defineProps({
9
- message: { type: null, required: true }
9
+ message: { type: null, required: true },
10
+ streamActive: { type: Boolean, required: false },
11
+ preparingSteps: { type: [Array, null], required: false },
12
+ showTtft: { type: Boolean, required: false, default: true }
10
13
  });
11
14
  const emit = defineEmits(["retry"]);
12
15
  const { user } = useChatAuth();
@@ -176,17 +179,24 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
176
179
  </template>
177
180
 
178
181
  <template v-else>
182
+ <!--
183
+ A live stream normally owns the pending state, so this block stays
184
+ out of its way. Stand-in steps are the exception: until the run
185
+ emits anything the stream has nothing to show, and that silence is
186
+ exactly what they cover.
187
+ -->
179
188
  <ChatReasoningThought
180
- v-if="hasReasoning || isPending"
189
+ v-if="hasReasoning || preparingSteps?.length && !message.content || isPending && !streamActive"
181
190
  :reasoning-data="message.reasoningData ?? []"
182
191
  :animate="wasEverPending"
183
192
  :completed="!isPending"
193
+ :preparing-steps="preparingSteps"
184
194
  :created-at="message.createdAt"
185
195
  :updated-at="message.updatedAt"
186
196
  />
187
197
 
188
198
  <div
189
- v-if="!isPending && message.content"
199
+ v-if="!isPending && message.content || isPending && streamActive"
190
200
  max-w-prose
191
201
  :class="[
192
202
  isFailed ? 'flex gap-12px items-start bg-subtle border border-solid rounded-12px w-full' : 'rounded-12px code-theme'
@@ -238,7 +248,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
238
248
  :custom-components="mergedCustomComponents"
239
249
  :custom-html-tags="mergedCustomHtmlTags"
240
250
  :render-code-blocks-as-pre="true"
241
- :typewriter="shouldAnimate"
251
+ :typewriter="isPending || shouldAnimate"
242
252
  />
243
253
  </ClientOnly>
244
254
  <ChatFilePreview
@@ -261,11 +271,12 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
261
271
  :custom-components="mergedCustomComponents"
262
272
  :custom-html-tags="mergedCustomHtmlTags"
263
273
  :render-code-blocks-as-pre="true"
264
- :typewriter="shouldAnimate"
274
+ :typewriter="isPending || shouldAnimate"
265
275
  text-black-900
266
276
  />
267
277
  </ClientOnly>
268
278
  <div
279
+ v-if="!isPending"
269
280
  mt-8px
270
281
  flex
271
282
  items-center
@@ -274,9 +285,9 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
274
285
  text-tertiary
275
286
  >
276
287
  <span>{{ formatTime(message.updatedAt || message.createdAt) }}</span>
277
- <TelaDotSeparator v-if="formattedTtft || hasMessageFeedback" />
288
+ <TelaDotSeparator v-if="showTtft && formattedTtft || hasMessageFeedback" />
278
289
  <div flex items-center gap-8px>
279
- <span v-if="formattedTtft" flex items-center gap-4px>
290
+ <span v-if="showTtft && formattedTtft" flex items-center gap-4px>
280
291
  {{ formattedTtft }}
281
292
  </span>
282
293
  <ChatMessageFeedback v-if="hasMessageFeedback" :message="message" />