@meistrari/chat-nuxt 1.6.0 → 1.8.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 +25 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +23 -2
  4. package/dist/runtime/components/MeistrariChatEmbed.vue +8 -1
  5. package/dist/runtime/components/chat/bash-widget.vue +3 -3
  6. package/dist/runtime/components/chat/doc-sdk-widget.vue +3 -3
  7. package/dist/runtime/components/chat/files-view.vue +3 -64
  8. package/dist/runtime/components/chat/generic-tool-widget.vue +3 -3
  9. package/dist/runtime/components/chat/grep-widget.vue +3 -3
  10. package/dist/runtime/components/chat/message-bubble.vue +1 -1
  11. package/dist/runtime/components/chat/message-input.d.vue.ts +2 -0
  12. package/dist/runtime/components/chat/message-input.vue +21 -5
  13. package/dist/runtime/components/chat/message-input.vue.d.ts +2 -0
  14. package/dist/runtime/components/chat/message-list.vue +1 -1
  15. package/dist/runtime/components/chat/mobile/files/file-preview.d.vue.ts +11 -0
  16. package/dist/runtime/components/chat/mobile/files/file-preview.vue +112 -0
  17. package/dist/runtime/components/chat/mobile/files/file-preview.vue.d.ts +11 -0
  18. package/dist/runtime/components/chat/mobile/files/files-view.d.vue.ts +10 -0
  19. package/dist/runtime/components/chat/mobile/files/files-view.vue +384 -0
  20. package/dist/runtime/components/chat/mobile/files/files-view.vue.d.ts +10 -0
  21. package/dist/runtime/components/chat/mobile/home/header.d.vue.ts +19 -0
  22. package/dist/runtime/components/chat/mobile/home/header.vue +55 -0
  23. package/dist/runtime/components/chat/mobile/home/header.vue.d.ts +19 -0
  24. package/dist/runtime/components/chat/mobile/home/recents.d.vue.ts +14 -0
  25. package/dist/runtime/components/chat/mobile/home/recents.vue +48 -0
  26. package/dist/runtime/components/chat/mobile/home/recents.vue.d.ts +14 -0
  27. package/dist/runtime/components/chat/mobile/home/suggestions.d.vue.ts +7 -0
  28. package/dist/runtime/components/chat/mobile/home/suggestions.vue +31 -0
  29. package/dist/runtime/components/chat/mobile/home/suggestions.vue.d.ts +7 -0
  30. package/dist/runtime/components/chat/mobile/primitives/bottom-sheet.d.vue.ts +21 -0
  31. package/dist/runtime/components/chat/mobile/primitives/bottom-sheet.vue +48 -0
  32. package/dist/runtime/components/chat/mobile/primitives/bottom-sheet.vue.d.ts +21 -0
  33. package/dist/runtime/components/chat/mobile/primitives/info-bubble.d.vue.ts +6 -0
  34. package/dist/runtime/components/chat/mobile/primitives/info-bubble.vue +50 -0
  35. package/dist/runtime/components/chat/mobile/primitives/info-bubble.vue.d.ts +6 -0
  36. package/dist/runtime/components/chat/mobile/shell/drawer.d.vue.ts +22 -0
  37. package/dist/runtime/components/chat/mobile/shell/drawer.vue +143 -0
  38. package/dist/runtime/components/chat/mobile/shell/drawer.vue.d.ts +22 -0
  39. package/dist/runtime/components/chat/mobile/shell/header.d.vue.ts +40 -0
  40. package/dist/runtime/components/chat/mobile/shell/header.vue +139 -0
  41. package/dist/runtime/components/chat/mobile/shell/header.vue.d.ts +40 -0
  42. package/dist/runtime/components/chat/mobile/shell/outdated-settings-sheet.d.vue.ts +14 -0
  43. package/dist/runtime/components/chat/mobile/shell/outdated-settings-sheet.vue +47 -0
  44. package/dist/runtime/components/chat/mobile/shell/outdated-settings-sheet.vue.d.ts +14 -0
  45. package/dist/runtime/components/chat/mobile/shell/shell.d.vue.ts +122 -0
  46. package/dist/runtime/components/chat/mobile/shell/shell.vue +283 -0
  47. package/dist/runtime/components/chat/mobile/shell/shell.vue.d.ts +122 -0
  48. package/dist/runtime/components/chat/mobile/shell/workspace-sheet.d.vue.ts +19 -0
  49. package/dist/runtime/components/chat/mobile/shell/workspace-sheet.vue +51 -0
  50. package/dist/runtime/components/chat/mobile/shell/workspace-sheet.vue.d.ts +19 -0
  51. package/dist/runtime/components/chat/mobile/usage/usage-view.d.vue.ts +8 -0
  52. package/dist/runtime/components/chat/mobile/usage/usage-view.vue +243 -0
  53. package/dist/runtime/components/chat/mobile/usage/usage-view.vue.d.ts +8 -0
  54. package/dist/runtime/components/chat/model-selector.vue +6 -0
  55. package/dist/runtime/components/chat/read-widget.vue +3 -3
  56. package/dist/runtime/components/chat/reasoning-step-item.vue +1 -1
  57. package/dist/runtime/components/chat/reasoning-steps.vue +1 -1
  58. package/dist/runtime/components/chat/reasoning-thought.vue +2 -3
  59. package/dist/runtime/components/chat/tela-skill-widget.vue +3 -3
  60. package/dist/runtime/components/chat/thinking-widget.vue +3 -3
  61. package/dist/runtime/components/chat/usage-view.vue +20 -101
  62. package/dist/runtime/components/chat/web-search-widget.vue +3 -3
  63. package/dist/runtime/components/chat/workstation-query-widget.vue +6 -6
  64. package/dist/runtime/components/chat/write-widget.vue +3 -3
  65. package/dist/runtime/composables/useChat.js +2 -1
  66. package/dist/runtime/composables/useChatApi.js +4 -0
  67. package/dist/runtime/composables/useConversationFileDownloads.d.ts +6 -0
  68. package/dist/runtime/composables/useConversationFileDownloads.js +50 -0
  69. package/dist/runtime/composables/useConversationUsage.d.ts +13 -0
  70. package/dist/runtime/composables/useConversationUsage.js +121 -0
  71. package/dist/runtime/composables/useConversations.js +3 -1
  72. package/dist/runtime/composables/useEmbedConfig.d.ts +4 -0
  73. package/dist/runtime/composables/useEmbedConfig.js +9 -0
  74. package/dist/runtime/composables/useMentionAutocomplete.d.ts +6 -0
  75. package/dist/runtime/composables/useMentionAutocomplete.js +71 -23
  76. package/dist/runtime/composables/useWorkspaceSwitchModal.d.ts +5 -0
  77. package/dist/runtime/composables/useWorkspaceSwitchModal.js +14 -0
  78. package/dist/runtime/embed/components/ChatConfigurationModal.vue +119 -13
  79. package/dist/runtime/embed/components/ChatEmbed.vue +9 -2
  80. package/dist/runtime/embed/components/ChatEmbedInner.d.vue.ts +2 -0
  81. package/dist/runtime/embed/components/ChatEmbedInner.vue +201 -11
  82. package/dist/runtime/embed/components/ChatEmbedInner.vue.d.ts +2 -0
  83. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.d.vue.ts +34 -0
  84. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.vue +95 -0
  85. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.vue.d.ts +34 -0
  86. package/dist/runtime/server/api/conversations/[id]/duplicate.post.js +1 -0
  87. package/dist/runtime/server/api/conversations/[id]/messages/[messageId]/retry.post.js +3 -1
  88. package/dist/runtime/server/api/conversations/[id]/messages/index.get.js +29 -5
  89. package/dist/runtime/server/api/conversations/index.post.js +1 -0
  90. package/dist/runtime/server/db/schema/conversations.d.ts +17 -0
  91. package/dist/runtime/server/db/schema/conversations.js +1 -0
  92. package/dist/runtime/server/utils/chat-context.d.ts +2 -0
  93. package/dist/runtime/server/utils/chat-context.js +15 -1
  94. package/dist/runtime/server/utils/conversation-message-sync.d.ts +8 -2
  95. package/dist/runtime/server/utils/conversation-message-sync.js +52 -6
  96. package/dist/runtime/server/utils/conversation-scope.js +2 -1
  97. package/dist/runtime/types/chat-auth.d.ts +9 -8
  98. package/dist/runtime/types/embed.d.ts +21 -0
  99. package/dist/runtime/utils/breakpoints.d.ts +1 -0
  100. package/dist/runtime/utils/breakpoints.js +1 -0
  101. package/dist/runtime/utils/tela-chat.d.ts +3 -1
  102. package/dist/runtime/utils/tela-chat.js +11 -2
  103. package/drizzle/0015_illegal_blindfold.sql +1 -0
  104. package/drizzle/meta/0015_snapshot.json +738 -0
  105. package/drizzle/meta/_journal.json +7 -0
  106. package/package.json +6 -1
package/README.md CHANGED
@@ -161,11 +161,18 @@ The chat workspace is resolved from the authenticated `activeOrganization` provi
161
161
  <MeistrariChatEmbed tela-agent-id="agent_123" />
162
162
  ```
163
163
 
164
+ `conversationScope` works in both runtimes. In default chat mode, it isolates conversations inside the authenticated workspace without requiring a Tela agent:
165
+
166
+ ```vue
167
+ <MeistrariChatEmbed :conversation-scope="`user:${userId}`" />
168
+ ```
169
+
164
170
  Host apps can also provide Tela agent inputs directly. When `telaAgentInputs` is non-null, the chat sends those inputs with each agent run and does not render the agent variables panel.
165
171
 
166
172
  ```vue
167
173
  <MeistrariChatEmbed
168
174
  tela-agent-id="agent_123"
175
+ :conversation-scope="`customer:${customerId}`"
169
176
  :tela-agent-inputs="[
170
177
  { type: 'text', name: 'customer_id', content: customerId },
171
178
  ]"
@@ -203,6 +210,7 @@ Key props for embedding:
203
210
  - **`:hide-sidebar="true"`** — removes the conversation list, keeps just the chat
204
211
  - **`v-model:conversation-id`** — sync the active conversation with your app's state
205
212
  - **`:initial-conversation-id`** — open a specific conversation on mount
213
+ - **`conversation-scope`** — isolate conversation history inside the same workspace or Tela agent
206
214
 
207
215
  ## `<MeistrariChatEmbed>` Props
208
216
 
@@ -211,6 +219,7 @@ Key props for embedding:
211
219
  | `hideSidebar` | `boolean` | `false` | Hide conversation list sidebar |
212
220
  | `conversationId` | `string \| null` | `null` | Controlled conversation (supports v-model) |
213
221
  | `initialConversationId` | `string \| null` | `null` | Open this conversation on mount |
222
+ | `conversationScope` | `string \| null` | `null` | Isolate conversation history by technical scope within the current workspace and runtime |
214
223
  | `telaAgentId` | `string` | — | Use Tela agent mode for this embed |
215
224
  | `telaAgentInputs` | `TelaAgentExecutionInput[] \| null` | `undefined` | Inputs sent with each Tela agent run; hides the variables panel when non-null |
216
225
  | `workspaceSettings` | `WorkspaceSettings \| null` | `null` | Override settings from host app |
@@ -221,6 +230,22 @@ Key props for embedding:
221
230
 
222
231
  `loadingMessagesMode` only applies when `loadingMessages` has at least one non-empty message.
223
232
 
233
+ Use `conversationScope` when you need multiple chat surfaces to share the same authenticated workspace but keep separate histories. This is useful for per-user inboxes, record detail pages, customer workspaces, template previews, workflow steps, or any app route where the same chat should only show conversations for that route's domain object. The scope is applied together with the workspace and, when present, the `telaAgentId`.
234
+
235
+ Scoped embeds only see conversations created with the same scope. Unscoped embeds only see unscoped conversations, so adding a scope will not mix with existing workspace-level history. Scope values are technical identifiers: they are trimmed, blank strings become `null`, and server requests accept 1-200 characters from `A-Z`, `a-z`, `0-9`, `.`, `_`, `:`, `/`, and `-`.
236
+
237
+ ```vue
238
+ <!-- Default chat mode: workspace + scope -->
239
+ <MeistrariChatEmbed :conversation-scope="`user:${userId}`" />
240
+ <MeistrariChatEmbed :conversation-scope="`customer:${customerId}`" />
241
+
242
+ <!-- Tela agent mode: workspace + telaAgentId + scope -->
243
+ <MeistrariChatEmbed
244
+ tela-agent-id="agent_123"
245
+ :conversation-scope="`template:${templateId}`"
246
+ />
247
+ ```
248
+
224
249
  **Events:**
225
250
 
226
251
  | Event | Payload | Description |
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "1.6.0",
4
+ "version": "1.8.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, mkdirSync, symlinkSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import { join, dirname } from 'node:path';
4
- import { defineNuxtModule, createResolver, addTemplate, addPlugin, addImportsDir, addComponent, addComponentsDir } from 'nuxt/kit';
4
+ import { defineNuxtModule, createResolver, addTemplate, addTypeTemplate, addPlugin, addImportsDir, addComponent, addComponentsDir } from 'nuxt/kit';
5
5
 
6
6
  const agentEnvironmentResolverCandidates = [
7
7
  "server/chat/resolve-agent-environment.ts",
@@ -179,6 +179,7 @@ const DEFAULT_FEATURES = {
179
179
  generateTitle: false,
180
180
  credentials: false
181
181
  };
182
+ const VUE_COMPONENT_EXTENSIONS = [".vue"];
182
183
  function resolveChatAuthMode(runtimeConfig) {
183
184
  const publicConfig = runtimeConfig.public;
184
185
  const telaAuth = publicConfig?.telaAuth;
@@ -195,6 +196,7 @@ export function useChatAuth() {
195
196
  return {
196
197
  user: auth.user,
197
198
  activeOrganization: auth.activeOrganization,
199
+ logout: auth.logout,
198
200
  getToken: auth.getToken,
199
201
  getAvailableOrganizations: auth.getAvailableOrganizations,
200
202
  switchOrganization: auth.switchOrganization,
@@ -240,6 +242,7 @@ export function useChatAuth() {
240
242
  return {
241
243
  user: session.user,
242
244
  activeOrganization: organization.activeOrganization,
245
+ logout: session.signOut,
243
246
  getToken: session.getToken,
244
247
  getAvailableOrganizations: organization.listOrganizations,
245
248
  switchOrganization: organization.setActiveOrganization,
@@ -342,6 +345,22 @@ const module$1 = defineNuxtModule({
342
345
  filename: "chat-nuxt/auth.mjs",
343
346
  getContents: () => buildChatAuthTemplate(chatAuthMode)
344
347
  });
348
+ const chatAuthTypeTemplate = addTypeTemplate({
349
+ filename: "chat-nuxt/chat-auth.d.ts",
350
+ src: join(runtimeDir, "types/chat-auth.d.ts")
351
+ });
352
+ addTypeTemplate({
353
+ filename: "chat-nuxt/chat-agent-environment-resolver.d.ts",
354
+ src: join(runtimeDir, "types/chat-agent-environment-resolver.d.ts")
355
+ }, {
356
+ nitro: true,
357
+ nuxt: true
358
+ });
359
+ nuxt.hook("prepare:types", ({ tsConfig }) => {
360
+ tsConfig.compilerOptions ||= {};
361
+ tsConfig.compilerOptions.paths ||= {};
362
+ tsConfig.compilerOptions.paths["#chat-auth"] = [chatAuthTypeTemplate.dst];
363
+ });
345
364
  if (!Array.isArray(nuxt.options.extends)) {
346
365
  nuxt.options.extends = nuxt.options.extends ? [nuxt.options.extends] : [];
347
366
  }
@@ -401,10 +420,12 @@ const module$1 = defineNuxtModule({
401
420
  priority: 1e3
402
421
  });
403
422
  addComponentsDir({
404
- path: join(runtimeDir, "components")
423
+ path: join(runtimeDir, "components"),
424
+ extensions: VUE_COMPONENT_EXTENSIONS
405
425
  });
406
426
  addComponentsDir({
407
427
  path: join(runtimeDir, "embed/components"),
428
+ extensions: VUE_COMPONENT_EXTENSIONS,
408
429
  pathPrefix: false
409
430
  });
410
431
  nuxt.hook("vite:extendConfig", (config, { isServer }) => {
@@ -1,8 +1,10 @@
1
1
  <script setup>
2
- defineProps({
2
+ const props = defineProps({
3
3
  conversationId: { type: [String, null], required: false },
4
4
  initialConversationId: { type: [String, null], required: false },
5
+ conversationScope: { type: [String, null], required: false },
5
6
  hideSidebar: { type: Boolean, required: false },
7
+ hideSettings: { type: Boolean, required: false },
6
8
  loadingMessages: { type: [Array, null], required: false },
7
9
  loadingMessagesMode: { type: [String, null], required: false },
8
10
  customComponents: { type: Object, required: false },
@@ -14,6 +16,7 @@ defineProps({
14
16
  features: { type: Object, required: false }
15
17
  });
16
18
  defineEmits(["update:conversationId", "action"]);
19
+ const hideSettings = computed(() => props.hideSettings ?? false);
17
20
  </script>
18
21
 
19
22
  <template>
@@ -23,7 +26,9 @@ defineEmits(["update:conversationId", "action"]);
23
26
  :tela-agent-inputs="telaAgentInputs"
24
27
  :conversation-id="conversationId"
25
28
  :initial-conversation-id="initialConversationId"
29
+ :conversation-scope="conversationScope"
26
30
  :hide-sidebar="hideSidebar"
31
+ :hide-settings="hideSettings"
27
32
  :features="features"
28
33
  :loading-messages="loadingMessages"
29
34
  :loading-messages-mode="loadingMessagesMode"
@@ -38,7 +43,9 @@ defineEmits(["update:conversationId", "action"]);
38
43
  :user="user"
39
44
  :conversation-id="conversationId"
40
45
  :initial-conversation-id="initialConversationId"
46
+ :conversation-scope="conversationScope"
41
47
  :hide-sidebar="hideSidebar"
48
+ :hide-settings="hideSettings"
42
49
  :features="features"
43
50
  :loading-messages="loadingMessages"
44
51
  :loading-messages-mode="loadingMessagesMode"
@@ -31,15 +31,15 @@ const isOutputCode = computed(() => {
31
31
  <template>
32
32
  <div class="bg-subtle rounded-16px border border-gray-200">
33
33
  <!-- Header -->
34
- <div class="px-24px py-24px flex items-center justify-between">
35
- <h3 class="text-20px font-580 leading-24px tracking-[-0.5px] text-primary truncate">
34
+ <div class="px-16px py-16px flex items-center justify-between">
35
+ <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary truncate">
36
36
  Bash
37
37
  </h3>
38
38
  <TelaStatus :variant="statusVariant" :label="statusLabel" />
39
39
  </div>
40
40
 
41
41
  <!-- Content -->
42
- <div v-if="toolResult" class="px-24px pb-24px space-y-16px">
42
+ <div v-if="toolResult" class="px-16px pb-16px space-y-16px">
43
43
  <!-- Command section -->
44
44
  <div>
45
45
  <div class="text-14px font-580 leading-16px tracking-[-0.15px] text-gray-700 mb-2">
@@ -31,15 +31,15 @@ const isOutputCode = computed(() => {
31
31
  <template>
32
32
  <div class="bg-subtle rounded-16px border border-gray-200">
33
33
  <!-- Header -->
34
- <div class="px-24px py-24px flex items-center justify-between">
35
- <h3 class="text-20px font-580 leading-24px tracking-[-0.5px] text-primary">
34
+ <div class="px-16px py-16px flex items-center justify-between">
35
+ <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary">
36
36
  {{ skillName }}
37
37
  </h3>
38
38
  <TelaStatus :variant="statusVariant" :label="statusLabel" />
39
39
  </div>
40
40
 
41
41
  <!-- Content -->
42
- <div v-if="toolResult" class="px-24px pb-24px space-y-16px">
42
+ <div v-if="toolResult" class="px-16px pb-16px space-y-16px">
43
43
  <!-- Result -->
44
44
  <div>
45
45
  <div class="text-14px font-580 leading-16px tracking-[-0.15px] text-gray-700 mb-2">
@@ -1,5 +1,4 @@
1
1
  <script setup>
2
- import { useClipboard } from "@vueuse/core";
3
2
  import { formatFileSize, formatUploadDate, getFileIcon, getFileTypeLabel } from "../../utils/file";
4
3
  const props = defineProps({
5
4
  conversationId: { type: String, required: true }
@@ -8,7 +7,7 @@ const conversationIdRef = computed(() => props.conversationId);
8
7
  const { files, loading } = useConversationFiles(conversationIdRef);
9
8
  const { openPreview, updatePreview, setError } = useFilePreviewPanel();
10
9
  const { getCached, fetchUrlAsBase64, setCache } = useMediaCache();
11
- const { copy } = useClipboard();
10
+ const { downloadFile, copyFileLink } = useConversationFileDownloads();
12
11
  const searchQuery = ref("");
13
12
  const fileTypeFilter = ref(null);
14
13
  const currentRequestId = ref(0);
@@ -144,77 +143,17 @@ async function handleFileClick(file) {
144
143
  });
145
144
  }
146
145
  }
147
- const statusToast = useStatusToast();
148
- async function handleDownload(file) {
149
- statusToast.update({
150
- text: "Baixando arquivo...",
151
- loading: true
152
- });
153
- try {
154
- let downloadUrl = file.url;
155
- if (file.url.startsWith("vault://")) {
156
- const { url } = await $fetch("/api/vault/resolve", {
157
- method: "POST",
158
- body: { reference: file.url }
159
- });
160
- downloadUrl = url;
161
- }
162
- const response = await fetch(downloadUrl);
163
- const blob = await response.blob();
164
- const blobUrl = URL.createObjectURL(blob);
165
- const link = document.createElement("a");
166
- link.href = blobUrl;
167
- link.download = file.name;
168
- document.body.appendChild(link);
169
- link.click();
170
- document.body.removeChild(link);
171
- URL.revokeObjectURL(blobUrl);
172
- statusToast.update({
173
- text: "Download conclu\xEDdo",
174
- icon: "i-ph-check"
175
- });
176
- } catch (error) {
177
- console.error("Failed to download file:", error);
178
- statusToast.update({
179
- text: "Erro ao baixar arquivo",
180
- icon: "i-ph-warning"
181
- });
182
- }
183
- }
184
- async function handleCopyLink(file) {
185
- try {
186
- let publicUrl = file.url;
187
- if (file.url.startsWith("vault://")) {
188
- const { url } = await $fetch("/api/vault/resolve", {
189
- method: "POST",
190
- body: { reference: file.url }
191
- });
192
- publicUrl = url;
193
- }
194
- await copy(publicUrl);
195
- statusToast.update({
196
- text: "Link copiado",
197
- icon: "i-ph-check"
198
- });
199
- } catch (error) {
200
- console.error("Failed to copy link:", error);
201
- statusToast.update({
202
- text: "Erro ao copiar link",
203
- icon: "i-ph-warning"
204
- });
205
- }
206
- }
207
146
  function getFileMenuItems(file) {
208
147
  return [
209
148
  {
210
149
  label: "Copiar link",
211
150
  icon: "i-ph-link",
212
- click: () => handleCopyLink(file)
151
+ click: () => copyFileLink(file)
213
152
  },
214
153
  {
215
154
  label: "Baixar",
216
155
  icon: "i-ph-download-simple",
217
- click: () => handleDownload(file)
156
+ click: () => downloadFile(file)
218
157
  }
219
158
  ];
220
159
  }
@@ -84,15 +84,15 @@ const isEmptyMessage = computed(() => {
84
84
  <template>
85
85
  <div class="bg-subtle rounded-16px border border-gray-200">
86
86
  <!-- Header -->
87
- <div class="px-24px py-24px flex items-center justify-between">
88
- <h3 class="text-20px font-580 leading-24px tracking-[-0.5px] text-primary truncate">
87
+ <div class="px-16px py-16px flex items-center justify-between">
88
+ <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary truncate">
89
89
  {{ name }}
90
90
  </h3>
91
91
  <TelaStatus :variant="statusVariant" :label="statusLabel" />
92
92
  </div>
93
93
 
94
94
  <!-- Content -->
95
- <div v-if="hasContent" class="px-24px pb-24px space-y-16px">
95
+ <div v-if="hasContent" class="px-16px pb-16px space-y-16px">
96
96
  <!-- Parameters section -->
97
97
  <div v-if="input && Object.keys(input).length > 0">
98
98
  <!-- Special handling for TodoWrite tool -->
@@ -122,9 +122,9 @@ function highlightMatch(content) {
122
122
  <template>
123
123
  <div class="bg-subtle rounded-16px border border-gray-200">
124
124
  <!-- Header -->
125
- <div class="px-24px py-24px flex items-center justify-between">
125
+ <div class="px-16px py-16px flex items-center justify-between">
126
126
  <div class="flex-1 min-w-0">
127
- <h3 class="text-20px font-580 leading-24px tracking-[-0.5px] text-primary truncate">
127
+ <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary truncate">
128
128
  Grep
129
129
  </h3>
130
130
  <p class="text-12px text-secondary truncate mt-2px">
@@ -136,7 +136,7 @@ function highlightMatch(content) {
136
136
  </div>
137
137
 
138
138
  <!-- Content -->
139
- <div v-if="toolResult" class="px-24px pb-24px space-y-16px">
139
+ <div v-if="toolResult" class="px-16px pb-16px space-y-16px">
140
140
  <!-- Search info -->
141
141
  <div>
142
142
  <div class="text-14px font-580 leading-16px tracking-[-0.15px] text-gray-700 mb-2">
@@ -201,7 +201,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
201
201
  v-if="hasReasoning || isPending"
202
202
  :reasoning-data="message.reasoningData ?? []"
203
203
  :animate="wasEverPending"
204
- :completed="message.status === 'completed'"
204
+ :completed="!isPending"
205
205
  :created-at="message.createdAt"
206
206
  :updated-at="message.updatedAt"
207
207
  />
@@ -20,12 +20,14 @@ type __VLS_Props = {
20
20
  showCancelButton?: boolean;
21
21
  };
22
22
  declare function addFiles(files: File[], inputName?: string): void;
23
+ declare function setDraft(text: string): void;
23
24
  type __VLS_ModelProps = {
24
25
  'model'?: ModelOption;
25
26
  };
26
27
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
27
28
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
28
29
  addFiles: typeof addFiles;
30
+ setDraft: typeof setDraft;
29
31
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
30
32
  "update:model": (value: "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6") => any;
31
33
  } & {
@@ -395,14 +395,31 @@ function addFiles(files, inputName) {
395
395
  });
396
396
  queueFiles(validFiles, inputName);
397
397
  }
398
+ function setDraft(text) {
399
+ content.value = text;
400
+ if (editorRef.value) {
401
+ editorRef.value.textContent = text;
402
+ const range = document.createRange();
403
+ range.selectNodeContents(editorRef.value);
404
+ range.collapse(false);
405
+ const selection = window.getSelection();
406
+ selection?.removeAllRanges();
407
+ if (range)
408
+ selection?.addRange(range);
409
+ }
410
+ nextTick(() => {
411
+ editorRef.value?.focus();
412
+ });
413
+ }
398
414
  defineExpose({
399
- addFiles
415
+ addFiles,
416
+ setDraft
400
417
  });
401
418
  </script>
402
419
 
403
420
  <template>
404
- <div :class="centered ? '' : hasDisclaimer ? 'px-24px' : 'px-24px pb-24px'" bg-transparent>
405
- <div :class="centered ? 'w-700px' : 'max-w-800px'" mx-auto>
421
+ <div :class="centered ? 'w-full' : hasDisclaimer ? 'px-24px' : 'px-24px pb-24px'" bg-transparent>
422
+ <div :class="centered ? 'w-full max-w-700px' : 'max-w-800px'" mx-auto>
406
423
  <Transition name="agent-inputs-panel-motion" mode="out-in" appear>
407
424
  <div
408
425
  v-if="hasAgentInputs"
@@ -511,7 +528,6 @@ defineExpose({
511
528
  </Transition>
512
529
  </div>
513
530
  </Transition>
514
-
515
531
  <div v-if="pendingFiles.length > 0" flex flex-wrap gap-8px mb-12px>
516
532
  <div
517
533
  v-for="pendingFile in pendingFiles"
@@ -668,5 +684,5 @@ defineExpose({
668
684
  </template>
669
685
 
670
686
  <style scoped>
671
- .input-container{border:.5px solid #e5e7eb;box-shadow:0 3px 10px 0 hsla(0,0%,64%,.08),0 12px 20px 0 hsla(0,0%,64%,.06);position:relative;z-index:2}.input-locked{background:var(--Base-Gray-gray-100,#f3f4f6);border:.5px solid var(--Base-Neutral-neutral-300,#d9d9d9);box-shadow:0 12px 20px 0 hsla(0,0%,64%,.06)}.agent-inputs-panel{box-shadow:0 3px 10px 0 hsla(0,0%,64%,.06);position:relative;transform-origin:bottom center;will-change:max-height,opacity,transform,filter;z-index:1}.agent-inputs-panel-motion-enter-active,.agent-inputs-panel-motion-leave-active{overflow:hidden;transition:max-height .26s cubic-bezier(.2,.8,.2,1),opacity .18s ease,transform .26s cubic-bezier(.2,.8,.2,1),filter .22s ease}.agent-inputs-panel-motion-enter-from,.agent-inputs-panel-motion-leave-to{filter:blur(2px);max-height:0;opacity:0;transform:translateY(46px) scale(.985)}.agent-inputs-panel-motion-enter-to,.agent-inputs-panel-motion-leave-from{filter:blur(0);max-height:560px;opacity:1;transform:translateY(0) scale(1)}.agent-inputs-content{transform-origin:bottom center;will-change:max-height,opacity,transform,filter}.agent-inputs-content-enter-active,.agent-inputs-content-leave-active{overflow:hidden;transition:max-height .22s cubic-bezier(.2,.8,.2,1),opacity .16s ease,transform .22s cubic-bezier(.2,.8,.2,1),filter .22s ease}.agent-inputs-content-enter-from,.agent-inputs-content-leave-to{filter:blur(2px);max-height:0;opacity:0;transform:translateY(14px) scale(.985)}.agent-inputs-content-enter-to,.agent-inputs-content-leave-from{filter:blur(0);max-height:900px;opacity:1;transform:translateY(0) scale(1)}.agent-text-input{border:.5px solid #e5e7eb;border-radius:8px;color:#111827;font-size:13px;line-height:18px;min-height:64px;outline:none;padding:8px 10px;resize:vertical;width:100%}.agent-text-input:focus{border-color:#d1d5db}.agent-text-input:disabled{background-color:#f3f4f6;cursor:not-allowed}.agent-file-variable-select{background:#fff;border:.5px solid #d1d5db;border-radius:6px;color:#374151;font-size:12px;height:28px;max-width:160px;outline:none;padding:0 8px}.editor-content{caret-color:#111827;color:#111827;max-height:200px;outline:none;overflow-y:auto;white-space:pre-wrap;word-break:break-word}.editor-content.is-empty:before{color:#bdc4cc;content:attr(data-placeholder);float:left;height:0;pointer-events:none}.icon-button:disabled{cursor:not-allowed;opacity:.5}.custom-scrollbar{scrollbar-color:#d1d5db transparent;scrollbar-width:thin}.custom-scrollbar::-webkit-scrollbar{width:6px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}.custom-scrollbar::-webkit-scrollbar-thumb{background-color:#d1d5db;border-radius:3px}.stop-button{align-items:center;background:transparent;border:none;cursor:pointer;display:flex;height:32px;justify-content:center;padding:0;position:relative;transition:opacity .15s ease;width:32px}.stop-button:hover{opacity:.8}.stop-button-ring{border:2px solid #e5e7eb;border-radius:50%;inset:0;position:absolute}.stop-button-ring:before{animation:spin 1s linear infinite;border:2px solid transparent;border-radius:50%;border-top-color:#111827;content:"";inset:-2px;position:absolute}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}
687
+ .input-container{border:.5px solid #e5e7eb;box-shadow:0 3px 10px 0 hsla(0,0%,64%,.08),0 12px 20px 0 hsla(0,0%,64%,.06);position:relative;z-index:2}.input-locked{background:var(--Base-Gray-gray-100,#f3f4f6);border:.5px solid var(--Base-Neutral-neutral-300,#d9d9d9);box-shadow:0 12px 20px 0 hsla(0,0%,64%,.06)}.agent-inputs-panel{box-shadow:0 3px 10px 0 hsla(0,0%,64%,.06);position:relative;transform-origin:bottom center;will-change:max-height,opacity,transform,filter;z-index:1}.agent-inputs-panel-motion-enter-active,.agent-inputs-panel-motion-leave-active{overflow:hidden;transition:max-height .26s cubic-bezier(.2,.8,.2,1),opacity .18s ease,transform .26s cubic-bezier(.2,.8,.2,1),filter .22s ease}.agent-inputs-panel-motion-enter-from,.agent-inputs-panel-motion-leave-to{filter:blur(2px);max-height:0;opacity:0;transform:translateY(46px) scale(.985)}.agent-inputs-panel-motion-enter-to,.agent-inputs-panel-motion-leave-from{filter:blur(0);max-height:560px;opacity:1;transform:translateY(0) scale(1)}.agent-inputs-content{transform-origin:bottom center;will-change:max-height,opacity,transform,filter}.agent-inputs-content-enter-active,.agent-inputs-content-leave-active{overflow:hidden;transition:max-height .22s cubic-bezier(.2,.8,.2,1),opacity .16s ease,transform .22s cubic-bezier(.2,.8,.2,1),filter .22s ease}.agent-inputs-content-enter-from,.agent-inputs-content-leave-to{filter:blur(2px);max-height:0;opacity:0;transform:translateY(14px) scale(.985)}.agent-inputs-content-enter-to,.agent-inputs-content-leave-from{filter:blur(0);max-height:900px;opacity:1;transform:translateY(0) scale(1)}.agent-text-input{border:.5px solid #e5e7eb;border-radius:8px;color:#111827;font-size:13px;line-height:18px;min-height:64px;outline:none;padding:8px 10px;resize:vertical;width:100%}.agent-text-input:focus{border-color:#d1d5db}.agent-text-input:disabled{background-color:#f3f4f6;cursor:not-allowed}.agent-file-variable-select{background:#fff;border:.5px solid #d1d5db;border-radius:6px;color:#374151;font-size:12px;height:28px;max-width:160px;outline:none;padding:0 8px}.editor-content{caret-color:#111827;color:#111827;max-height:200px;outline:none;overflow-y:auto;white-space:pre-wrap;word-break:break-word}.editor-content.is-empty:before{color:#bdc4cc;content:attr(data-placeholder);float:left;height:0;pointer-events:none}.icon-button:disabled{cursor:not-allowed;opacity:.5}.custom-scrollbar{scrollbar-color:#d1d5db transparent;scrollbar-width:thin}.custom-scrollbar::-webkit-scrollbar{width:6px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}.custom-scrollbar::-webkit-scrollbar-thumb{background-color:#d1d5db;border-radius:3px}@media (max-width:480px){.custom-scrollbar{scrollbar-width:none}.custom-scrollbar::-webkit-scrollbar{display:none}}.stop-button{align-items:center;background:transparent;border:none;cursor:pointer;display:flex;height:32px;justify-content:center;padding:0;position:relative;transition:opacity .15s ease;width:32px}.stop-button:hover{opacity:.8}.stop-button-ring{border:2px solid #e5e7eb;border-radius:50%;inset:0;position:absolute}.stop-button-ring:before{animation:spin 1s linear infinite;border:2px solid transparent;border-radius:50%;border-top-color:#111827;content:"";inset:-2px;position:absolute}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}
672
688
  </style>
@@ -20,12 +20,14 @@ type __VLS_Props = {
20
20
  showCancelButton?: boolean;
21
21
  };
22
22
  declare function addFiles(files: File[], inputName?: string): void;
23
+ declare function setDraft(text: string): void;
23
24
  type __VLS_ModelProps = {
24
25
  'model'?: ModelOption;
25
26
  };
26
27
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
27
28
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
28
29
  addFiles: typeof addFiles;
30
+ setDraft: typeof setDraft;
29
31
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
30
32
  "update:model": (value: "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6") => any;
31
33
  } & {
@@ -27,7 +27,7 @@ onMounted(scrollToBottom);
27
27
  ref="containerRef"
28
28
  flex-1
29
29
  overflow-y-auto
30
- pt-88px pb-24px px-24px
30
+ pt-72px pb-24px px-16px
31
31
  >
32
32
  <div max-w-800px mx-auto>
33
33
  <div v-if="loading" flex="~ col" gap-16px>
@@ -0,0 +1,11 @@
1
+ import type { ConversationFile } from '#chat-runtime/types/chat';
2
+ type __VLS_Props = {
3
+ file: ConversationFile | null;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
6
+ close: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
+ onClose?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,112 @@
1
+ <script setup>
2
+ import { onKeyStroke } from "@vueuse/core";
3
+ import { formatFileSize, getFileTypeLabel, normalizeMimeType } from "#chat-runtime/utils/file";
4
+ const props = defineProps({
5
+ file: { type: null, required: true }
6
+ });
7
+ const emit = defineEmits(["close"]);
8
+ const { resolveDownloadUrl, downloadFile } = useConversationFileDownloads();
9
+ const normalizedMimeType = computed(() => normalizeMimeType(props.file?.mimeType ?? ""));
10
+ const isImage = computed(() => normalizedMimeType.value.startsWith("image/"));
11
+ const resolvedUrl = ref(null);
12
+ const resolving = ref(false);
13
+ watch(() => props.file, async (file) => {
14
+ resolvedUrl.value = null;
15
+ if (!file) {
16
+ return;
17
+ }
18
+ if (!file.url.startsWith("vault://")) {
19
+ resolvedUrl.value = file.url;
20
+ return;
21
+ }
22
+ resolving.value = true;
23
+ try {
24
+ const url = await resolveDownloadUrl(file);
25
+ if (props.file?.id === file.id) {
26
+ resolvedUrl.value = url;
27
+ }
28
+ } catch (err) {
29
+ console.error("Failed to resolve preview URL", err);
30
+ } finally {
31
+ resolving.value = false;
32
+ }
33
+ }, { immediate: true });
34
+ onKeyStroke("Escape", () => {
35
+ if (props.file) {
36
+ emit("close");
37
+ }
38
+ });
39
+ function handleDownload() {
40
+ if (props.file) {
41
+ void downloadFile(props.file);
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <Transition name="preview">
48
+ <div
49
+ v-if="file"
50
+ class="preview"
51
+ :class="isImage ? 'preview--image' : 'preview--surface'"
52
+ role="dialog"
53
+ aria-modal="true"
54
+ >
55
+ <div class="preview__topbar" :class="isImage ? 'preview__topbar--dark' : 'preview__topbar--light'">
56
+ <button
57
+ class="preview__icon-btn"
58
+ aria-label="Fechar"
59
+ @click="emit('close')"
60
+ >
61
+ <span i-ph-x />
62
+ </button>
63
+
64
+ <div flex="~ col 1" min-w-0 px-8px>
65
+ <span class="preview__title" truncate>{{ file.name }}</span>
66
+ <span class="preview__meta" truncate>
67
+ {{ getFileTypeLabel(file.mimeType) }}<template v-if="file.size">
68
+ · {{ formatFileSize(file.size) }}
69
+ </template>
70
+ </span>
71
+ </div>
72
+
73
+ <button
74
+ class="preview__icon-btn"
75
+ aria-label="Baixar"
76
+ @click="handleDownload"
77
+ >
78
+ <span i-ph-download-simple />
79
+ </button>
80
+ </div>
81
+
82
+ <div class="preview__body">
83
+ <div v-if="resolving && !resolvedUrl" flex items-center justify-center h-full w-full>
84
+ <span
85
+ i-ph-spinner animate-spin text-32px
86
+ :class="isImage ? 'text-white' : 'text-neutral-400'"
87
+ />
88
+ </div>
89
+
90
+ <img
91
+ v-else-if="isImage && resolvedUrl"
92
+ :src="resolvedUrl"
93
+ :alt="file.name"
94
+ class="preview__image"
95
+ >
96
+
97
+ <RenderersArtifactDisplay
98
+ v-else-if="resolvedUrl"
99
+ :url="resolvedUrl"
100
+ :mime-type="normalizedMimeType"
101
+ :name="file.name"
102
+ class="preview__renderer"
103
+ @download="handleDownload"
104
+ />
105
+ </div>
106
+ </div>
107
+ </Transition>
108
+ </template>
109
+
110
+ <style scoped>
111
+ .preview{display:flex;flex-direction:column;inset:0;position:absolute;z-index:50}.preview--image{background:#0a0a0b}.preview--surface{background:#fff}.preview-enter-active,.preview-leave-active{transition:transform .26s cubic-bezier(.2,.8,.2,1)}.preview-enter-from,.preview-leave-to{transform:translateY(100%)}.preview__topbar{align-items:center;display:flex;flex-shrink:0;gap:4px;padding:10px 10px 8px}.preview__topbar--dark{background:rgba(0,0,0,.4);color:#fff}.preview__topbar--light{border-bottom:.5px solid #e8e8e8;color:#171717}.preview__icon-btn{align-items:center;background:transparent;border-radius:8px;color:inherit;display:inline-flex;font-size:20px;height:32px;justify-content:center;transition:background .15s;width:32px}.preview__icon-btn:hover{background:hsla(0,0%,100%,.08)}.preview--surface .preview__icon-btn:hover{background:rgba(0,0,0,.05)}.preview__title{display:block;font-size:14px;font-weight:600;letter-spacing:-.15px;line-height:18px}.preview__meta{display:block;font-size:11px;line-height:14px;opacity:.8}.preview__body{align-items:center;display:flex;flex:1;justify-content:center;min-height:0;overflow:auto}.preview--image .preview__body{overflow:hidden}.preview__image{max-height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain}.preview__renderer{display:block;min-height:100%;width:100%}
112
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { ConversationFile } from '#chat-runtime/types/chat';
2
+ type __VLS_Props = {
3
+ file: ConversationFile | null;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
6
+ close: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
+ onClose?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,10 @@
1
+ type __VLS_Props = {
2
+ conversationId: string;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
5
+ viewInChat: () => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
7
+ onViewInChat?: (() => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;