@meistrari/chat-nuxt 4.6.0-rc.1 → 4.6.0-rc.10

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 (37) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/conversations/components/chat-conversation-item.d.vue.ts +1 -0
  3. package/dist/runtime/conversations/components/chat-conversation-item.vue +3 -2
  4. package/dist/runtime/conversations/components/chat-conversation-item.vue.d.ts +1 -0
  5. package/dist/runtime/conversations/components/chat-conversation-list.d.vue.ts +4 -1
  6. package/dist/runtime/conversations/components/chat-conversation-list.vue +1 -1
  7. package/dist/runtime/conversations/components/chat-conversation-list.vue.d.ts +4 -1
  8. package/dist/runtime/conversations/composables/conversation-item.d.ts +1 -0
  9. package/dist/runtime/conversations/composables/conversation-list.d.ts +5 -1
  10. package/dist/runtime/conversations/composables/conversation-list.js +3 -1
  11. package/dist/runtime/embed/components/chat-embed-inner.vue +2 -2
  12. package/dist/runtime/embed/components/chat-embed.vue +1 -0
  13. package/dist/runtime/embed/components/meistrari-chat-embed.vue +2 -1
  14. package/dist/runtime/embed/components/mobile/chat-mobile-shell.d.vue.ts +2 -2
  15. package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue.d.ts +2 -2
  16. package/dist/runtime/embed/types.d.ts +2 -0
  17. package/dist/runtime/lightweight/messages/composables/use-direct-stream.d.ts +4 -4
  18. package/dist/runtime/lightweight/messages/composables/use-direct-stream.js +10 -3
  19. package/dist/runtime/messages/components/reasoning/chat-reasoning-message.vue +23 -7
  20. package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue +3 -0
  21. package/dist/runtime/messages/composables/chat.js +7 -28
  22. package/dist/runtime/messages/debug/chat-debug-scenarios.js +105 -0
  23. package/dist/runtime/messages/message-reasoning.d.ts +0 -1
  24. package/dist/runtime/messages/message-reasoning.js +21 -10
  25. package/dist/runtime/messages/message-state.d.ts +11 -0
  26. package/dist/runtime/messages/message-state.js +37 -0
  27. package/dist/runtime/messages/reasoning-step-key.js +2 -0
  28. package/dist/runtime/messages/reasoning.d.ts +5 -1
  29. package/dist/runtime/messages/tool-registry.js +17 -0
  30. package/dist/runtime/rendering/components/app-markdown-render.vue +2 -1
  31. package/dist/runtime/rendering/markdown/normalize-custom-tag-json.d.ts +1 -0
  32. package/dist/runtime/rendering/markdown/normalize-custom-tag-json.js +15 -0
  33. package/dist/runtime/server/utils/chat-api-proxy.js +4 -0
  34. package/dist/runtime/types/schemas/integrations/lightweight.js +2 -2
  35. package/dist/runtime/workspace/composables/workspace-members.d.ts +0 -1
  36. package/dist/runtime/workspace/composables/workspace-members.js +0 -4
  37. package/package.json +2 -2
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "4.6.0-rc.1",
4
+ "version": "4.6.0-rc.10",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -6,6 +6,7 @@ type __VLS_Props = {
6
6
  draggable?: boolean;
7
7
  itemClass?: HTMLAttributes['class'];
8
8
  userImage?: string;
9
+ userName?: string;
9
10
  selectConversation?: (id: string) => void | Promise<void>;
10
11
  copyLink?: (id: string) => void | Promise<void>;
11
12
  debug?: (id: string) => void;
@@ -6,6 +6,7 @@ const props = defineProps({
6
6
  draggable: { type: Boolean, required: false },
7
7
  itemClass: { type: [Boolean, null, String, Object, Array], required: false, skipCheck: true },
8
8
  userImage: { type: String, required: false },
9
+ userName: { type: String, required: false },
9
10
  selectConversation: { type: Function, required: false },
10
11
  copyLink: { type: Function, required: false },
11
12
  debug: { type: Function, required: false }
@@ -53,13 +54,13 @@ function setRenameInputRef(element) {
53
54
  <div flex items-center justify-between h-full :class="props.itemClass">
54
55
  <!-- User Avatar -->
55
56
  <div
56
- v-if="userImage"
57
+ v-if="userName"
57
58
  translate-x--48px w-0 opacity-0 shrink-0 overflow-hidden
58
59
  class="[transition:width_0.26s_cubic-bezier(0.22,1,0.36,1),transform_0.26s_cubic-bezier(0.22,1,0.36,1),opacity_0.2s] group-hover:w-22px group-hover:translate-x-0 group-hover:opacity-100"
59
60
  >
60
61
  <TelaAvatar
61
62
  :image="userImage"
62
- alt="Usuário"
63
+ :alt="userName"
63
64
  size="2xs"
64
65
  class="rounded-full!"
65
66
  />
@@ -6,6 +6,7 @@ type __VLS_Props = {
6
6
  draggable?: boolean;
7
7
  itemClass?: HTMLAttributes['class'];
8
8
  userImage?: string;
9
+ userName?: string;
9
10
  selectConversation?: (id: string) => void | Promise<void>;
10
11
  copyLink?: (id: string) => void | Promise<void>;
11
12
  debug?: (id: string) => void;
@@ -3,7 +3,10 @@ import type { Conversation } from '../../types/schemas/chat/conversations.js';
3
3
  type __VLS_Props = {
4
4
  conversations: DeepReadonly<Conversation[]>;
5
5
  currentId?: string;
6
- getMemberImage?: (userId: string) => string | undefined;
6
+ getMember?: (userId: string) => {
7
+ name: string;
8
+ image?: string;
9
+ } | undefined;
7
10
  selectConversation?: (id: string) => void | Promise<void>;
8
11
  resetConversation?: () => void | Promise<void>;
9
12
  startConversation?: (groupId?: string | null) => void | Promise<void>;
@@ -4,7 +4,7 @@ import { useConversationList } from "../composables/conversation-list";
4
4
  const props = defineProps({
5
5
  conversations: { type: null, required: true },
6
6
  currentId: { type: String, required: false },
7
- getMemberImage: { type: Function, required: false },
7
+ getMember: { type: Function, required: false },
8
8
  selectConversation: { type: Function, required: false },
9
9
  resetConversation: { type: Function, required: false },
10
10
  startConversation: { type: Function, required: false },
@@ -3,7 +3,10 @@ import type { Conversation } from '../../types/schemas/chat/conversations.js';
3
3
  type __VLS_Props = {
4
4
  conversations: DeepReadonly<Conversation[]>;
5
5
  currentId?: string;
6
- getMemberImage?: (userId: string) => string | undefined;
6
+ getMember?: (userId: string) => {
7
+ name: string;
8
+ image?: string;
9
+ } | undefined;
7
10
  selectConversation?: (id: string) => void | Promise<void>;
8
11
  resetConversation?: () => void | Promise<void>;
9
12
  startConversation?: (groupId?: string | null) => void | Promise<void>;
@@ -5,6 +5,7 @@ export type ConversationItemOptions = {
5
5
  active: boolean;
6
6
  draggable?: boolean;
7
7
  userImage?: string;
8
+ userName?: string;
8
9
  selectConversation?: (id: string) => void | Promise<void>;
9
10
  copyLink?: (id: string) => void | Promise<void>;
10
11
  debug?: (id: string) => void;
@@ -3,7 +3,10 @@ import type { Conversation } from '../../types/schemas/chat/conversations.js';
3
3
  export type ConversationListOptions = {
4
4
  conversations: DeepReadonly<Conversation[]>;
5
5
  currentId?: string;
6
- getMemberImage?: (userId: string) => string | undefined;
6
+ getMember?: (userId: string) => {
7
+ name: string;
8
+ image?: string;
9
+ } | undefined;
7
10
  selectConversation?: (id: string) => void | Promise<void>;
8
11
  resetConversation?: () => void | Promise<void>;
9
12
  startConversation?: (groupId?: string | null) => void | Promise<void>;
@@ -120,6 +123,7 @@ export declare function useConversationList(options: ConversationListOptions): {
120
123
  active: boolean;
121
124
  draggable: boolean;
122
125
  userImage: string | undefined;
126
+ userName: string | undefined;
123
127
  selectConversation: ((id: string) => void | Promise<void>) | undefined;
124
128
  copyLink: (conversationId: string) => Promise<void>;
125
129
  debug: (() => void) | undefined;
@@ -492,11 +492,13 @@ export function useConversationList(options) {
492
492
  return draggedConversationId.value !== null && dropTargetGroupId.value === groupId;
493
493
  }
494
494
  function getConversationItemProps(conversation) {
495
+ const author = options.getMember?.(conversation.userId);
495
496
  return {
496
497
  conversation,
497
498
  active: conversation.id === options.currentId,
498
499
  draggable: true,
499
- userImage: options.getMemberImage?.(conversation.userId),
500
+ userImage: author?.image,
501
+ userName: author?.name,
500
502
  selectConversation: options.selectConversation,
501
503
  copyLink: copyConversationLink,
502
504
  debug: options.showDebugOption ? () => {
@@ -85,7 +85,7 @@ const route = useRoute();
85
85
  const embedConfig = useEmbedConfig();
86
86
  const chatAction = useChatAction();
87
87
  const { user: authUser, activeOrganization, logout } = useChatAuth();
88
- const { getMember, getMemberImage } = useWorkspaceMembers();
88
+ const { getMember } = useWorkspaceMembers();
89
89
  const statusToast = useStatusToast();
90
90
  const {
91
91
  conversations,
@@ -928,7 +928,7 @@ async function handleDelete() {
928
928
  <ChatConversationList
929
929
  :conversations="visibleConversations"
930
930
  :current-id="activeConversationId ?? void 0"
931
- :get-member-image="getMemberImage"
931
+ :get-member="getMember"
932
932
  :select-conversation="openConversation"
933
933
  :reset-conversation="handleNewConversation"
934
934
  :start-conversation="handleNewConversation"
@@ -42,6 +42,7 @@ const props = defineProps({
42
42
  user: { type: null, required: false },
43
43
  features: { type: Object, required: false },
44
44
  variant: { type: String, required: false },
45
+ position: { type: String, required: false },
45
46
  defaultOpen: { type: Boolean, required: false },
46
47
  closeOnOutsideClick: { type: Boolean, required: false },
47
48
  keepMounted: { type: Boolean, required: false },
@@ -34,6 +34,7 @@ const { hideSidebar = void 0, syncBrowserState = true, closeOnOutsideClick = tru
34
34
  user: { type: null, required: false },
35
35
  features: { type: Object, required: false },
36
36
  variant: { type: String, required: false },
37
+ position: { type: String, required: false },
37
38
  defaultOpen: { type: Boolean, required: false },
38
39
  closeOnOutsideClick: { type: Boolean, required: false },
39
40
  keepMounted: { type: Boolean, required: false },
@@ -132,7 +133,7 @@ const widgetEmbedProps = computed(() => ({
132
133
 
133
134
  <ClientOnly v-else>
134
135
  <!-- Floating: popover anchored to the launcher pill -->
135
- <TelaChatFloatingWindow v-if="variant === 'floating'" :open="isOpen" :close-on-outside-click="closeOnOutsideClick" :keep-mounted="props.keepMounted" @update:open="setOpen">
136
+ <TelaChatFloatingWindow v-if="variant === 'floating'" :open="isOpen" :position="props.position" :close-on-outside-click="closeOnOutsideClick" :keep-mounted="props.keepMounted" @update:open="setOpen">
136
137
  <template #launcher>
137
138
  <TelaChatFloatingWindowTrigger :open="isOpen" :label="props.launcherLabel" />
138
139
  </template>
@@ -67,8 +67,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
67
67
  copyLink: () => any;
68
68
  debug: () => any;
69
69
  selectConversation: (id: string) => any;
70
- send: (content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any;
71
70
  settings: () => any;
71
+ send: (content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any;
72
72
  tabChange: (tab: TopbarTab) => any;
73
73
  newConversation: (groupId?: string | null | undefined) => any;
74
74
  switchWorkspace: () => any;
@@ -88,8 +88,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
88
88
  onCopyLink?: (() => any) | undefined;
89
89
  onDebug?: (() => any) | undefined;
90
90
  onSelectConversation?: ((id: string) => any) | undefined;
91
- onSend?: ((content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any) | undefined;
92
91
  onSettings?: (() => any) | undefined;
92
+ onSend?: ((content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any) | undefined;
93
93
  "onUpdate:selectedModel"?: ((value: string | null) => any) | undefined;
94
94
  "onUpdate:selectedReasoningEffort"?: ((value: any) => any) | undefined;
95
95
  onTabChange?: ((tab: TopbarTab) => any) | undefined;
@@ -67,8 +67,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
67
67
  copyLink: () => any;
68
68
  debug: () => any;
69
69
  selectConversation: (id: string) => any;
70
- send: (content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any;
71
70
  settings: () => any;
71
+ send: (content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any;
72
72
  tabChange: (tab: TopbarTab) => any;
73
73
  newConversation: (groupId?: string | null | undefined) => any;
74
74
  switchWorkspace: () => any;
@@ -88,8 +88,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
88
88
  onCopyLink?: (() => any) | undefined;
89
89
  onDebug?: (() => any) | undefined;
90
90
  onSelectConversation?: ((id: string) => any) | undefined;
91
- onSend?: ((content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any) | undefined;
92
91
  onSettings?: (() => any) | undefined;
92
+ onSend?: ((content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any) | undefined;
93
93
  "onUpdate:selectedModel"?: ((value: string | null) => any) | undefined;
94
94
  "onUpdate:selectedReasoningEffort"?: ((value: any) => any) | undefined;
95
95
  onTabChange?: ((tab: TopbarTab) => any) | undefined;
@@ -189,6 +189,8 @@ export type ChatEmbedVariantProps = {
189
189
  * client-only and default to `hideSidebar`.
190
190
  */
191
191
  variant?: ChatEmbedVariant;
192
+ /** Floating variant only: launcher corner and animation origin. Defaults to right. */
193
+ position?: 'left' | 'right';
192
194
  /** Floating variant only: start with the widget window open. */
193
195
  defaultOpen?: boolean;
194
196
  /** Floating variant only: close on outside clicks. Defaults to true. */
@@ -1,17 +1,17 @@
1
1
  import { applyLightweightPart } from '../../../types/schemas/integrations/lightweight.js';
2
2
  import type { LightweightTurn } from '../../../types/schemas/integrations/lightweight.js';
3
+ import { createMessagePartsAccumulator as createClaudeMessagePartsAccumulator } from '../../../messages/composables/use-direct-stream.js';
3
4
  import type { DirectStreamOptions, MessagePartsEvent } from '../../../messages/composables/use-direct-stream.js';
4
5
  type MessagePartsAccumulator = {
5
6
  runId: string | null;
6
7
  lightweight?: LightweightTurn;
8
+ claude?: ReturnType<typeof createClaudeMessagePartsAccumulator>;
7
9
  };
8
10
  export declare function createMessagePartsAccumulator(runId?: string | null): MessagePartsAccumulator;
9
11
  export declare function applyMessagePartsEvent(accumulator: MessagePartsAccumulator, input: MessagePartsEvent): {
10
- text: any;
11
- thinking: any;
12
+ text: string;
13
+ thinking: string;
12
14
  changed: boolean;
13
- reasoning?: undefined;
14
- files?: undefined;
15
15
  } | {
16
16
  text: any;
17
17
  thinking: any;
@@ -1,13 +1,20 @@
1
1
  import { applyLightweightPart, createLightweightTurn } from "../../../types/schemas/integrations/lightweight.js";
2
- import { useDirectStreamWithProtocol } from "../../../messages/composables/use-direct-stream.js";
2
+ import {
3
+ applyMessagePartsEvent as applyClaudeMessagePartsEvent,
4
+ createMessagePartsAccumulator as createClaudeMessagePartsAccumulator,
5
+ useDirectStreamWithProtocol
6
+ } from "../../../messages/composables/use-direct-stream.js";
3
7
  export function createMessagePartsAccumulator(runId = null) {
4
8
  return {
5
9
  runId
6
10
  };
7
11
  }
8
12
  export function applyMessagePartsEvent(accumulator, input) {
9
- if (input.harness !== "lightweight" && accumulator.lightweight?.runId !== input.runId)
10
- return { text: accumulator.lightweight?.stepText ?? "", thinking: accumulator.lightweight?.thinking ?? "", changed: false };
13
+ if (input.harness !== "lightweight" && accumulator.lightweight?.runId !== input.runId) {
14
+ accumulator.claude ??= createClaudeMessagePartsAccumulator();
15
+ accumulator.runId = input.runId;
16
+ return applyClaudeMessagePartsEvent(accumulator.claude, input);
17
+ }
11
18
  if (accumulator.lightweight?.runId !== input.runId)
12
19
  accumulator.lightweight = createLightweightTurn(input.runId, input.sessionId ?? "");
13
20
  accumulator.runId = input.runId;
@@ -27,24 +27,25 @@ const TOOL_WIDGETS = {
27
27
  "grep": ChatGrepWidget,
28
28
  "generic": ChatGenericToolWidget
29
29
  };
30
- const displayToolUse = computed(() => typeof props.step === "string" ? null : withReportedToolInput(props.step.toolUse, props.step.toolResult));
30
+ const toolGroup = computed(() => typeof props.step !== "string" && props.step.type === "tool_group" ? props.step : null);
31
+ const displayToolUse = computed(() => toolGroup.value ? withReportedToolInput(toolGroup.value.toolUse, toolGroup.value.toolResult) : null);
31
32
  const toolDisplay = computed(() => {
32
- if (typeof props.step === "string")
33
+ if (!toolGroup.value)
33
34
  return null;
34
- return resolveToolDisplay(props.step.toolUse);
35
+ return resolveToolDisplay(toolGroup.value.toolUse);
35
36
  });
36
37
  const stepWasExecuted = computed(
37
- () => typeof props.step !== "string" && props.step.toolResult !== void 0 && !props.isActive
38
+ () => toolGroup.value?.toolResult !== void 0 && !props.isActive
38
39
  );
39
40
  const stepMeta = computed(() => {
40
- if (typeof props.step === "string")
41
+ if (!toolGroup.value)
41
42
  return null;
42
43
  return resolveToolMeta(displayToolUse.value);
43
44
  });
44
45
  const stepTitle = computed(() => {
45
- if (typeof props.step === "string")
46
+ if (!toolGroup.value)
46
47
  return "";
47
- return resolveToolTitle(props.step.toolUse, stepWasExecuted.value);
48
+ return resolveToolTitle(toolGroup.value.toolUse, stepWasExecuted.value);
48
49
  });
49
50
  </script>
50
51
 
@@ -61,6 +62,21 @@ const stepTitle = computed(() => {
61
62
  />
62
63
  </div>
63
64
 
65
+ <div v-else-if="step.type === 'thinking'" class="chat-reasoning-step">
66
+ <ChatReasoningToolRow
67
+ :title="isActive && !completed ? 'Pensando' : 'Pensou'"
68
+ has-result
69
+ :is-active="isActive && !completed"
70
+ >
71
+ <ChatThinkingWidget
72
+ class="chat-reasoning-thinking-detail"
73
+ :content="step.text"
74
+ :is-active="isActive"
75
+ :completed="completed"
76
+ />
77
+ </ChatReasoningToolRow>
78
+ </div>
79
+
64
80
  <div v-else class="chat-reasoning-step">
65
81
  <ChatReasoningToolRow
66
82
  :title="stepTitle"
@@ -34,6 +34,9 @@ const currentStepName = computed(() => {
34
34
  if (step && typeof step !== "string" && step.type === "tool_group") {
35
35
  return resolveToolTitle(step.toolUse, false);
36
36
  }
37
+ if (step && typeof step !== "string" && step.type === "thinking") {
38
+ return "Pensando";
39
+ }
37
40
  if (step && typeof step === "string") {
38
41
  const truncated = step.length > TEXT_TRUNCATE_LENGTH ? `${step.slice(0, TEXT_TRUNCATE_LENGTH)}...` : step;
39
42
  return truncated;
@@ -8,6 +8,7 @@ import {
8
8
  } from "../message-reasoning.js";
9
9
  import {
10
10
  beginAssistantMessageRetry,
11
+ createOptimisticMessagePair,
11
12
  markPendingAssistantMessagesFailed,
12
13
  markPendingAssistantMessagesInterrupted,
13
14
  restoreAssistantMessageAfterRetryFailure,
@@ -341,7 +342,7 @@ export function useChat(options = {}, adapter = {}) {
341
342
  if (generation !== pollGeneration)
342
343
  return;
343
344
  const statusCode = getChatApiErrorStatus(err);
344
- const terminalError = statusCode === 501 || statusCode !== null && statusCode >= 400 && statusCode < 500 && statusCode !== 408 && statusCode !== 429;
345
+ const terminalError = statusCode === 501 || statusCode !== null && statusCode >= 400 && statusCode < 500 && statusCode !== 401 && statusCode !== 408 && statusCode !== 429;
345
346
  consecutivePollFailures++;
346
347
  pollOutageStartedAt ??= Date.now();
347
348
  pollInterval.value = Math.min(POLL_INTERVAL_MS * 2 ** consecutivePollFailures, POLL_MAX_BACKOFF_MS);
@@ -531,36 +532,14 @@ export function useChat(options = {}, adapter = {}) {
531
532
  const conversationId = currentConversation.value?.id ?? "";
532
533
  const existingOptimisticUserMessage = queuedSend ? messages.value.find((message) => message.id === queuedSend.optimisticIds.user) : void 0;
533
534
  const existingOptimisticAssistantMessage = queuedSend ? messages.value.find((message) => message.id === queuedSend.optimisticIds.assistant) : void 0;
534
- const optimisticUserMessage = existingOptimisticUserMessage ?? {
535
- id: crypto.randomUUID(),
535
+ const optimisticPair = createOptimisticMessagePair({
536
536
  conversationId,
537
- role: "user",
538
537
  content,
539
- status: "completed",
540
- reasoningData: null,
541
538
  files: filesWithSource,
542
- externalUuid: null,
543
- ttft: null,
544
- createdBy: authUser.value?.email ?? null,
545
- createdAt: /* @__PURE__ */ new Date(),
546
- generationStartedAt: null,
547
- updatedAt: null
548
- };
549
- const optimisticAssistantMessage = existingOptimisticAssistantMessage ?? {
550
- id: crypto.randomUUID(),
551
- conversationId,
552
- role: "assistant",
553
- content: "",
554
- status: "pending",
555
- reasoningData: null,
556
- files: null,
557
- externalUuid: null,
558
- ttft: null,
559
- createdBy: null,
560
- createdAt: /* @__PURE__ */ new Date(),
561
- generationStartedAt: /* @__PURE__ */ new Date(),
562
- updatedAt: null
563
- };
539
+ createdBy: authUser.value?.email ?? null
540
+ });
541
+ const optimisticUserMessage = existingOptimisticUserMessage ?? optimisticPair.user;
542
+ const optimisticAssistantMessage = existingOptimisticAssistantMessage ?? optimisticPair.assistant;
564
543
  streamingMessageId.value = optimisticAssistantMessage.id;
565
544
  directStream.beginMessage();
566
545
  if (!queuedSend)
@@ -8,6 +8,7 @@ import { applyLightweightPart, createLightweightTurn } from "../../types/schemas
8
8
  import { applyMessagePartsEvent as applyLightweightEvent, createMessagePartsAccumulator as createLightweightAccumulator } from "../../lightweight/messages/composables/use-direct-stream.js";
9
9
  import { resolveReasoningDisplayMessages } from "../message-reasoning.js";
10
10
  import { overlayStreamedMessage } from "../stream-overlay.js";
11
+ import { createOptimisticMessagePair } from "../message-state.js";
11
12
  export const EMPTY_CHAT_DEBUG_PLAYBACK_STATE = {
12
13
  messages: [],
13
14
  loading: false,
@@ -589,6 +590,17 @@ export function createChatDebugScenarios(baseTime = Date.now()) {
589
590
  role: "assistant",
590
591
  status: "pending"
591
592
  });
593
+ const optimisticSendIds = ["ffffffff-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002"];
594
+ const optimisticSend = createOptimisticMessagePair({
595
+ conversationId: "debug-conversation",
596
+ content: "Resuma o parecer jur\xEDdico anexado em tr\xEAs pontos.",
597
+ files: null,
598
+ createdBy: "debugger@tela.com",
599
+ now: new Date(baseTime),
600
+ createId: () => optimisticSendIds.shift()
601
+ });
602
+ const acknowledgedSendAt = new Date(baseTime + 40);
603
+ const acknowledgedAssistantAt = new Date(acknowledgedSendAt.getTime() + 1);
592
604
  const allToolsReasoning = allToolTypeReasoning();
593
605
  const allToolsIntro = allToolsReasoning[0];
594
606
  const allToolGroups = allToolsReasoning.slice(1);
@@ -762,6 +774,99 @@ export function createChatDebugScenarios(baseTime = Date.now()) {
762
774
  })
763
775
  ]
764
776
  },
777
+ {
778
+ id: "optimistic-send-order",
779
+ name: "Optimistic send order",
780
+ intent: 'A fresh send renders prompt then "Trabalhando" from the first frame, and keeps that order when the server swaps in persisted ids.',
781
+ durationMs: 5500,
782
+ events: [
783
+ event({
784
+ id: "optimistic-send-user",
785
+ atMs: 0,
786
+ label: "Optimistic user message",
787
+ tone: "user",
788
+ type: "message.upsert",
789
+ message: optimisticSend.user
790
+ }),
791
+ event({
792
+ id: "optimistic-send-assistant",
793
+ atMs: 0,
794
+ label: "Optimistic assistant",
795
+ tone: "assistant",
796
+ type: "message.upsert",
797
+ message: optimisticSend.assistant
798
+ }),
799
+ event({
800
+ id: "optimistic-send-stream",
801
+ atMs: 0,
802
+ label: "Stream target",
803
+ tone: "neutral",
804
+ type: "playback.patch",
805
+ patch: { streamingActiveMessageId: optimisticSend.assistant.id }
806
+ }),
807
+ event({
808
+ id: "optimistic-send-ack-user",
809
+ atMs: 2500,
810
+ label: "Server persists user",
811
+ tone: "user",
812
+ type: "message.patch",
813
+ messageId: optimisticSend.user.id,
814
+ patch: { id: "optimistic-send-user", createdAt: acknowledgedSendAt }
815
+ }),
816
+ event({
817
+ id: "optimistic-send-ack-assistant",
818
+ atMs: 2500,
819
+ label: "Server persists assistant",
820
+ tone: "assistant",
821
+ type: "message.patch",
822
+ messageId: optimisticSend.assistant.id,
823
+ patch: { id: "optimistic-send-assistant", createdAt: acknowledgedAssistantAt, generationStartedAt: acknowledgedAssistantAt }
824
+ }),
825
+ event({
826
+ id: "optimistic-send-ack-stream",
827
+ atMs: 2500,
828
+ label: "Stream target persisted",
829
+ tone: "neutral",
830
+ type: "playback.patch",
831
+ patch: { streamingActiveMessageId: "optimistic-send-assistant" }
832
+ }),
833
+ event({
834
+ id: "optimistic-send-reasoning",
835
+ atMs: 3300,
836
+ label: "Reasoning starts",
837
+ tone: "reasoning",
838
+ type: "message.patch",
839
+ messageId: "optimistic-send-assistant",
840
+ patch: { reasoningData: ["Lendo o parecer anexado"] }
841
+ }),
842
+ event({
843
+ id: "optimistic-send-answer",
844
+ atMs: 4300,
845
+ label: "Answer",
846
+ tone: "assistant",
847
+ type: "message.patch",
848
+ messageId: "optimistic-send-assistant",
849
+ patch: { content: "1. O contrato \xE9 v\xE1lido.\n2. A cl\xE1usula penal \xE9 abusiva.\n3. Cabe renegocia\xE7\xE3o." }
850
+ }),
851
+ event({
852
+ id: "optimistic-send-complete",
853
+ atMs: 5e3,
854
+ label: "Message completed",
855
+ tone: "assistant",
856
+ type: "message.patch",
857
+ messageId: "optimistic-send-assistant",
858
+ patch: { status: "completed", updatedAt: new Date(baseTime + 5e3) }
859
+ }),
860
+ event({
861
+ id: "optimistic-send-close",
862
+ atMs: 5e3,
863
+ label: "Stream closes",
864
+ tone: "neutral",
865
+ type: "playback.patch",
866
+ patch: { streamingActiveMessageId: null }
867
+ })
868
+ ]
869
+ },
765
870
  {
766
871
  id: "lightweight-stream",
767
872
  name: "Lightweight stream",
@@ -2,7 +2,6 @@ import type { Message } from '../types/schemas/chat/conversations.js';
2
2
  import type { ReasoningStep } from './reasoning.js';
3
3
  type ParsedReasoning = {
4
4
  steps: ReasoningStep[];
5
- thinkingTexts: Set<string>;
6
5
  currentAnswerParts: string[];
7
6
  currentAnswerStepTexts: Set<string>;
8
7
  };
@@ -7,6 +7,8 @@ export function createReasoningCache() {
7
7
  function getReasoningStepKey(step) {
8
8
  if (typeof step === "string")
9
9
  return `text:${step.slice(0, 100)}`;
10
+ if (step.type === "thinking")
11
+ return `thinking:${step.text.slice(0, 100)}`;
10
12
  return `tool:${step.toolUse.id}`;
11
13
  }
12
14
  function groupReasoningSteps(parsedSteps) {
@@ -15,6 +17,14 @@ function groupReasoningSteps(parsedSteps) {
15
17
  for (const step of parsedSteps) {
16
18
  if (typeof step === "string") {
17
19
  grouped.push(step);
20
+ } else if ("type" in step) {
21
+ const previous = grouped.at(-1);
22
+ if (typeof previous === "object" && previous.type === "thinking")
23
+ grouped[grouped.length - 1] = { type: "thinking", text: `${previous.text}
24
+
25
+ ${step.text}` };
26
+ else
27
+ grouped.push(step);
18
28
  } else if ("name" in step && "input" in step) {
19
29
  pendingToolUses.set(step.id, step);
20
30
  grouped.push({
@@ -77,7 +87,7 @@ function parseReasoningFromPolling(reasoning) {
77
87
  const allSteps = [];
78
88
  const seenContentIds = /* @__PURE__ */ new Set();
79
89
  const seenTexts = /* @__PURE__ */ new Set();
80
- const thinkingTexts = /* @__PURE__ */ new Set();
90
+ const seenThoughts = /* @__PURE__ */ new Set();
81
91
  let currentAnswerParts = [];
82
92
  let currentAnswerStepTexts = /* @__PURE__ */ new Set();
83
93
  for (const item of reasoning) {
@@ -117,17 +127,18 @@ function parseReasoningFromPolling(reasoning) {
117
127
  if (!contentPart || typeof contentPart !== "object")
118
128
  continue;
119
129
  const part = contentPart;
120
- if ((part.type === "text" || part.type === "thinking") && typeof part.text === "string") {
121
- if (part.type === "text")
122
- answerParts.push(part.text);
123
- if (part.type === "thinking")
124
- thinkingTexts.add(part.text);
130
+ if (part.type === "thinking" && typeof part.text === "string") {
131
+ if (seenThoughts.has(part.text))
132
+ continue;
133
+ seenThoughts.add(part.text);
134
+ allSteps.push({ type: "thinking", text: part.text });
135
+ } else if (part.type === "text" && typeof part.text === "string") {
136
+ answerParts.push(part.text);
125
137
  if (seenTexts.has(part.text))
126
138
  continue;
127
139
  seenTexts.add(part.text);
128
140
  allSteps.push(part.text);
129
- if (part.type === "text")
130
- answerStepTexts.add(part.text);
141
+ answerStepTexts.add(part.text);
131
142
  } else if (part.type === "tool_use" && part.id && part.name) {
132
143
  const toolId = part.id;
133
144
  if (seenContentIds.has(`tool_use:${toolId}`))
@@ -183,7 +194,7 @@ function parseReasoningFromPolling(reasoning) {
183
194
  });
184
195
  }
185
196
  }
186
- return { steps: groupReasoningSteps(allSteps), thinkingTexts, currentAnswerParts, currentAnswerStepTexts };
197
+ return { steps: groupReasoningSteps(allSteps), currentAnswerParts, currentAnswerStepTexts };
187
198
  }
188
199
  export function resolveReasoningDisplayMessages(messages, cache = createReasoningCache()) {
189
200
  pruneReasoningCache(cache, messages);
@@ -198,7 +209,7 @@ export function resolveReasoningDisplayMessages(messages, cache = createReasonin
198
209
  const key = getReasoningStepKey(step);
199
210
  if (renderedSteps.has(key))
200
211
  continue;
201
- if (typeof step === "string" && currentAnswerMatches && parsed.currentAnswerStepTexts.has(step) && !parsed.thinkingTexts.has(step)) {
212
+ if (typeof step === "string" && currentAnswerMatches && parsed.currentAnswerStepTexts.has(step)) {
202
213
  continue;
203
214
  }
204
215
  renderedSteps.add(key);
@@ -2,6 +2,17 @@ import type { Message } from '../types/schemas/chat/conversations.js';
2
2
  export declare const INTERRUPTED_GENERATION_TEXT = "_Gera\u00E7\u00E3o interrompida pelo usu\u00E1rio_";
3
3
  export declare const GENERATION_FAILED_TEXT = "Erro: N\u00E3o foi poss\u00EDvel concluir a resposta";
4
4
  export declare const CANCELLATION_FAILED_TEXT = "Erro: N\u00E3o foi poss\u00EDvel cancelar a mensagem";
5
+ export declare function createOptimisticMessagePair(input: {
6
+ conversationId: string;
7
+ content: string;
8
+ files: Message['files'];
9
+ createdBy: string | null;
10
+ now?: Date;
11
+ createId?: () => string;
12
+ }): {
13
+ user: Message;
14
+ assistant: Message;
15
+ };
5
16
  export declare function markPendingAssistantMessagesInterrupted(messages: Message[]): Message[];
6
17
  export declare function markPendingAssistantMessagesFailed(messages: Message[], content?: string): Message[];
7
18
  export declare function beginAssistantMessageRetry(messages: Message[], messageId: string): {
@@ -1,6 +1,43 @@
1
1
  export const INTERRUPTED_GENERATION_TEXT = "_Gera\xE7\xE3o interrompida pelo usu\xE1rio_";
2
2
  export const GENERATION_FAILED_TEXT = "Erro: N\xE3o foi poss\xEDvel concluir a resposta";
3
3
  export const CANCELLATION_FAILED_TEXT = "Erro: N\xE3o foi poss\xEDvel cancelar a mensagem";
4
+ export function createOptimisticMessagePair(input) {
5
+ const createId = input.createId ?? (() => crypto.randomUUID());
6
+ const userCreatedAt = input.now ?? /* @__PURE__ */ new Date();
7
+ const assistantCreatedAt = new Date(userCreatedAt.getTime() + 1);
8
+ return {
9
+ user: {
10
+ id: createId(),
11
+ conversationId: input.conversationId,
12
+ role: "user",
13
+ content: input.content,
14
+ status: "completed",
15
+ reasoningData: null,
16
+ files: input.files,
17
+ externalUuid: null,
18
+ ttft: null,
19
+ createdBy: input.createdBy,
20
+ createdAt: userCreatedAt,
21
+ generationStartedAt: null,
22
+ updatedAt: null
23
+ },
24
+ assistant: {
25
+ id: createId(),
26
+ conversationId: input.conversationId,
27
+ role: "assistant",
28
+ content: "",
29
+ status: "pending",
30
+ reasoningData: null,
31
+ files: null,
32
+ externalUuid: null,
33
+ ttft: null,
34
+ createdBy: null,
35
+ createdAt: assistantCreatedAt,
36
+ generationStartedAt: assistantCreatedAt,
37
+ updatedAt: null
38
+ }
39
+ };
40
+ }
4
41
  export function markPendingAssistantMessagesInterrupted(messages) {
5
42
  return messages.map((message) => {
6
43
  if (message.role !== "assistant" || message.status !== "pending")
@@ -1,5 +1,7 @@
1
1
  export function getReasoningStepKey(step, index) {
2
2
  if (typeof step === "string")
3
3
  return `text-${index}`;
4
+ if (step.type === "thinking")
5
+ return `thinking-${index}`;
4
6
  return `tool-${step.toolUse.id}`;
5
7
  }
@@ -14,7 +14,11 @@ export type ToolGroup = {
14
14
  toolUse: ToolUse;
15
15
  toolResult?: ToolResult;
16
16
  };
17
- export type ReasoningStep = string | ToolGroup;
17
+ export type ThinkingStep = {
18
+ type: 'thinking';
19
+ text: string;
20
+ };
21
+ export type ReasoningStep = string | ToolGroup | ThinkingStep;
18
22
  /**
19
23
  * A turn that is still working towards its first output has no reasoning of its
20
24
  * own yet, so the preparing steps stand in. That makes the wait read as an agent
@@ -9,6 +9,11 @@ const TOOL_REGISTRY = {
9
9
  widgetId: "generic",
10
10
  labels: { active: "Pesquisando na web", executed: "Pesquisou na web" }
11
11
  },
12
+ "super_search_tool": {
13
+ icon: "i-ph-globe",
14
+ widgetId: "generic",
15
+ labels: { active: "Pesquisando em v\xE1rias fontes", executed: "Pesquisou em v\xE1rias fontes" }
16
+ },
12
17
  "WebFetch": {
13
18
  icon: "i-ph-browser",
14
19
  widgetId: "generic",
@@ -152,6 +157,16 @@ function stringInput(toolUse, ...keys) {
152
157
  }
153
158
  return null;
154
159
  }
160
+ function batchedQueries(toolUse) {
161
+ const queries = toolUse.input?.queries;
162
+ if (!Array.isArray(queries))
163
+ return null;
164
+ const filled = queries.filter((query) => typeof query === "string" && query.trim() !== "");
165
+ if (!filled.length)
166
+ return null;
167
+ const first = filled[0].trim();
168
+ return filled.length > 1 ? `${first} +${filled.length - 1}` : first;
169
+ }
155
170
  function basename(path) {
156
171
  return path.split("/").filter(Boolean).at(-1) ?? path;
157
172
  }
@@ -164,6 +179,8 @@ export function resolveToolMeta(toolUse) {
164
179
  return stringInput(toolUse, "pattern", "query");
165
180
  if (["WebSearch", "WebFetch", "web_search"].includes(toolUse.name))
166
181
  return stringInput(toolUse, "query", "url");
182
+ if (toolUse.name === "super_search_tool")
183
+ return stringInput(toolUse, "query") ?? batchedQueries(toolUse);
167
184
  if (toolUse.name === "Bash" || toolUse.name === "Execute")
168
185
  return stringInput(toolUse, "description", "command");
169
186
  if (toolUse.name === "Skill")
@@ -3,6 +3,7 @@ import MarkdownRender, { removeCustomComponents, setCustomComponents } from "mar
3
3
  import { wrapCustomMarkdownComponents } from "../markdown/custom-components";
4
4
  import { STREAMING_TEXT_FADE_MS } from "../markdown/streaming-text-segments";
5
5
  import { escapeSingleDollarSigns } from "../markdown/escape-dollar-signs";
6
+ import { normalizeCustomTagJson } from "../markdown/normalize-custom-tag-json";
6
7
  import StreamingTextNode from "../markdown/streaming-text-node.vue";
7
8
  const props = defineProps({
8
9
  content: { type: [String, null], required: false, default: "" },
@@ -15,7 +16,7 @@ const props = defineProps({
15
16
  const attrs = useAttrs();
16
17
  const instance = getCurrentInstance();
17
18
  const scopeId = `app-markdown-render-${instance?.uid ?? Math.random().toString(36).slice(2)}`;
18
- const escapedContent = computed(() => escapeSingleDollarSigns(props.content ?? ""));
19
+ const escapedContent = computed(() => escapeSingleDollarSigns(normalizeCustomTagJson(props.content ?? "", props.customHtmlTags)));
19
20
  const customMarkdownComponents = computed(() => wrapCustomMarkdownComponents(props.customComponents));
20
21
  const streamingTextActive = ref(props.typewriter);
21
22
  let streamingTextTimer;
@@ -0,0 +1 @@
1
+ export declare function normalizeCustomTagJson(text: string, customHtmlTags: readonly string[]): string;
@@ -0,0 +1,15 @@
1
+ export function normalizeCustomTagJson(text, customHtmlTags) {
2
+ if (!customHtmlTags.length)
3
+ return text;
4
+ return text.replace(/(^[ \t]{0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:\n[ \t]{0,3}(?=\2)(?:`+|~+)[ \t]*(?=\n|$)|(?![\s\S]))|(`+)[\s\S]*?\3)|^([ \t]{0,3}<([a-z][\w-]*)(?:\s+[\w-]+=(?:"[^"\n]*"|'[^'\n]*'))*\s+[\w-]+=')([\[{][^\n]*[\]}])([ \t]*\/>)[ \t]*(?=\n|$)/gm, (match, code, _fence, _inline, prefix, tag, json, closing) => {
5
+ if (code || !customHtmlTags.includes(tag))
6
+ return match;
7
+ try {
8
+ JSON.parse(json);
9
+ } catch {
10
+ return match;
11
+ }
12
+ const escaped = json.replace(/&/g, "\\u0026").replace(/'/g, "\\u0027").replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
13
+ return `${prefix}${escaped}'${closing}`;
14
+ });
15
+ }
@@ -206,6 +206,10 @@ export async function proxyChatApiRequest(event, targetPath, options = {}) {
206
206
  networkErrorCodes: networkErrorCodes(error)
207
207
  }, "Chat API proxy upstream request failed");
208
208
  }
209
+ if (event.node.res.headersSent) {
210
+ event.node.res.destroy();
211
+ return;
212
+ }
209
213
  throw error;
210
214
  }
211
215
  }
@@ -99,7 +99,7 @@ export function applyLightweightPart(state, part, sequence, timestamp) {
99
99
  const id = `${state.runId}:thinking:${typeof part.id === 'string' ? part.id : 'default'}`;
100
100
  const existing = state.reasoning.find(item => item.uuid === id)?.message.content[0];
101
101
  const text = existing && 'text' in existing && typeof existing.text === 'string' ? existing.text : '';
102
- upsertReasoning(state, reasoningItem(state.sessionId, id, { type: 'text', text: text + delta }, timestamp));
102
+ upsertReasoning(state, reasoningItem(state.sessionId, id, { type: 'thinking', text: text + delta }, timestamp));
103
103
  return true;
104
104
  }
105
105
  if (part.type === 'tool-call' && typeof part.toolCallId === 'string') {
@@ -134,7 +134,7 @@ export function lightweightThreadReasoning(sessionId, runId, events) {
134
134
  for (const [index, event] of events.entries()) {
135
135
  const timestamp = typeof event.timestamp === 'number' ? event.timestamp : 0;
136
136
  if ((event.type === 'thinking' || event.type === 'text') && typeof event.text === 'string') {
137
- upsertReasoning(state, reasoningItem(sessionId, `${runId}:${event.type}:${event.sequenceIndex ?? index}`, { type: 'text', text: event.text }, timestamp));
137
+ upsertReasoning(state, reasoningItem(sessionId, `${runId}:${event.type}:${event.sequenceIndex ?? index}`, { type: event.type === 'thinking' ? 'thinking' : 'text', text: event.text }, timestamp));
138
138
  }
139
139
  else if (event.type === 'tool_call') {
140
140
  applyLightweightPart(state, { type: 'tool-call', toolCallId: event.callId, toolName: event.toolName, input: event.arguments }, index, timestamp);
@@ -6,7 +6,6 @@ type MemberUser = {
6
6
  export declare function useWorkspaceMembers(): {
7
7
  members: any;
8
8
  membersMap: any;
9
- getMemberImage: (userId: string) => string | undefined;
10
9
  getMember: (userId: string) => MemberUser | undefined;
11
10
  getMemberByEmail: (email: string) => MemberUser | undefined;
12
11
  };
@@ -19,9 +19,6 @@ export function useWorkspaceMembers() {
19
19
  }
20
20
  return map;
21
21
  });
22
- function getMemberImage(userId) {
23
- return membersMap.value.get(userId)?.image ?? void 0;
24
- }
25
22
  function getMember(userId) {
26
23
  return membersMap.value.get(userId);
27
24
  }
@@ -31,7 +28,6 @@ export function useWorkspaceMembers() {
31
28
  return {
32
29
  members,
33
30
  membersMap,
34
- getMemberImage,
35
31
  getMember,
36
32
  getMemberByEmail
37
33
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
- "version": "4.6.0-rc.1",
3
+ "version": "4.6.0-rc.10",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -53,7 +53,7 @@
53
53
  "@iconify/vue": "^5.0.0",
54
54
  "@meistrari/auth-nuxt": "3.31.0",
55
55
  "@meistrari/logger": "^2.1.3",
56
- "@meistrari/tela-build": "^1.76.0",
56
+ "@meistrari/tela-build": "^1.79.0",
57
57
  "@sentry/nuxt": "^10.0.0",
58
58
  "@vueuse/components": "^12.8.0",
59
59
  "@vueuse/core": "^12.8.0",