@budibase/frontend-core 3.25.2 → 3.25.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "3.25.2",
3
+ "version": "3.25.4",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -9,6 +9,7 @@
9
9
  "check:types": "yarn svelte-check"
10
10
  },
11
11
  "dependencies": {
12
+ "@ai-sdk/svelte": "^4.0.48",
12
13
  "@budibase/bbui": "*",
13
14
  "@budibase/shared-core": "*",
14
15
  "@budibase/types": "*",
@@ -18,5 +19,5 @@
18
19
  "shortid": "2.2.15",
19
20
  "socket.io-client": "^4.7.5"
20
21
  },
21
- "gitHead": "f65a21f6595ff1a6d7b94d3f3ee9187185cc89a8"
22
+ "gitHead": "4a51aa9f4675ffa6555788aeaf6c31b522a952b3"
22
23
  }
@@ -1,52 +1,147 @@
1
1
  <script lang="ts">
2
- import { MarkdownViewer, notifications } from "@budibase/bbui"
2
+ import {
3
+ MarkdownViewer,
4
+ notifications,
5
+ Icon,
6
+ ProgressCircle,
7
+ } from "@budibase/bbui"
3
8
  import type {
4
9
  ChatConversation,
5
10
  DraftChatConversation,
6
11
  AgentMessageMetadata,
7
- ChatConversationRequest,
8
12
  } from "@budibase/types"
9
13
  import { Header } from "@budibase/shared-core"
10
14
  import BBAI from "../../icons/BBAI.svelte"
11
15
  import { tick } from "svelte"
12
- import { onDestroy } from "svelte"
13
- import { onMount } from "svelte"
14
- import { createEventDispatcher } from "svelte"
15
16
  import { createAPIClient } from "@budibase/frontend-core"
16
- import { v4 as uuidv4 } from "uuid"
17
- import type { UIMessage } from "ai"
18
-
19
- export let workspaceId: string
20
- export let API = createAPIClient({
21
- attachHeaders: headers => {
22
- if (workspaceId) {
23
- headers[Header.APP_ID] = workspaceId
24
- }
25
- },
26
- })
17
+ import { Chat } from "@ai-sdk/svelte"
18
+ import {
19
+ DefaultChatTransport,
20
+ isTextUIPart,
21
+ isToolUIPart,
22
+ isReasoningUIPart,
23
+ getToolName,
24
+ type UIMessage,
25
+ } from "ai"
26
+
27
27
  type ChatConversationLike = ChatConversation | DraftChatConversation
28
28
 
29
- export let chat: ChatConversationLike
30
- export let loading: boolean = false
31
- export let persistConversation: boolean = true
29
+ interface Props {
30
+ workspaceId: string
31
+ chat: ChatConversationLike
32
+ persistConversation?: boolean
33
+ onchatsaved?: (_event: {
34
+ detail: { chatId?: string; chat: ChatConversationLike }
35
+ }) => void
36
+ }
37
+
38
+ let {
39
+ workspaceId,
40
+ chat = $bindable(),
41
+ persistConversation = true,
42
+ onchatsaved,
43
+ }: Props = $props()
44
+
45
+ let API = $state(
46
+ createAPIClient({
47
+ attachHeaders: headers => {
48
+ if (workspaceId) {
49
+ headers[Header.APP_ID] = workspaceId
50
+ }
51
+ },
52
+ })
53
+ )
54
+
55
+ let chatAreaElement = $state<HTMLDivElement>()
56
+ let textareaElement = $state<HTMLTextAreaElement>()
57
+ let expandedTools = $state<Record<string, boolean>>({})
58
+ let inputValue = $state("")
59
+
60
+ let resolvedChatAppId = $state<string | undefined>()
61
+ let resolvedConversationId = $state<string | undefined>()
62
+
63
+ const chatInstance = new Chat<UIMessage<AgentMessageMetadata>>({
64
+ transport: new DefaultChatTransport({
65
+ headers: () => ({ [Header.APP_ID]: workspaceId }),
66
+ prepareSendMessagesRequest: ({ messages }) => {
67
+ const chatAppId = resolvedChatAppId || chat?.chatAppId
68
+ const conversationId = resolvedConversationId || chat?._id || "new"
69
+ return {
70
+ api: `/api/chatapps/${chatAppId}/conversations/${conversationId}/stream`,
71
+ body: {
72
+ _id: resolvedConversationId || chat?._id,
73
+ chatAppId,
74
+ agentId: chat?.agentId,
75
+ transient: !persistConversation,
76
+ title: chat?.title,
77
+ messages,
78
+ },
79
+ }
80
+ },
81
+ }),
82
+ messages: chat?.messages || [],
83
+ onFinish: async () => {
84
+ if (persistConversation && !chat._id && chat.chatAppId) {
85
+ try {
86
+ const history = await API.fetchChatHistory(chat.chatAppId)
87
+ const msgs = chatInstance.messages
88
+ const lastMessageId = msgs[msgs.length - 1]?.id
89
+ const savedConversation =
90
+ history?.find(convo =>
91
+ convo.messages.some(message => message.id === lastMessageId)
92
+ ) || history?.[0]
93
+
94
+ if (savedConversation) {
95
+ chat = { ...chat, ...savedConversation }
96
+ resolvedConversationId = savedConversation._id
97
+ }
98
+ } catch (historyError) {
99
+ console.error(historyError)
100
+ }
101
+ }
32
102
 
33
- const dispatch = createEventDispatcher<{
34
- chatSaved: { chatId?: string; chat: ChatConversationLike }
35
- }>()
103
+ chat = { ...chat, messages: chatInstance.messages }
104
+ onchatsaved?.({ detail: { chatId: chat._id, chat } })
36
105
 
37
- let inputValue = ""
38
- let chatAreaElement: HTMLDivElement
39
- let observer: MutationObserver
40
- let textareaElement: HTMLTextAreaElement
41
- let lastFocusedChatId: string | undefined
42
- let lastFocusedNewChat: ChatConversationLike | undefined
106
+ await tick()
107
+ textareaElement?.focus()
108
+ },
109
+ onError: error => {
110
+ console.error(error)
111
+ notifications.error(error.message || "Failed to send message")
112
+ },
113
+ })
114
+
115
+ let messages = $derived(chatInstance.messages)
116
+ let isBusy = $derived(
117
+ chatInstance.status === "streaming" || chatInstance.status === "submitted"
118
+ )
119
+
120
+ let lastChatId = $state<string | undefined>(chat?._id)
121
+ $effect(() => {
122
+ if (chat?._id !== lastChatId) {
123
+ lastChatId = chat?._id
124
+ chatInstance.messages = chat?.messages || []
125
+ expandedTools = {}
126
+ }
127
+ })
43
128
 
44
- $: if (chat?.messages?.length) {
45
- scrollToBottom()
129
+ const scrollToBottom = async () => {
130
+ await tick()
131
+ if (chatAreaElement) {
132
+ chatAreaElement.scrollTop = chatAreaElement.scrollHeight
133
+ }
46
134
  }
47
135
 
136
+ $effect(() => {
137
+ if (messages?.length) {
138
+ scrollToBottom()
139
+ }
140
+ })
141
+
48
142
  const ensureChatApp = async (): Promise<string | undefined> => {
49
143
  if (chat?.chatAppId) {
144
+ resolvedChatAppId = chat.chatAppId
50
145
  return chat.chatAppId
51
146
  }
52
147
  try {
@@ -66,6 +161,7 @@
66
161
  ? { agentId: fallbackAgentId }
67
162
  : {}),
68
163
  }
164
+ resolvedChatAppId = chatApp._id
69
165
  return chatApp._id
70
166
  }
71
167
  } catch (err) {
@@ -74,28 +170,21 @@
74
170
  return undefined
75
171
  }
76
172
 
77
- async function scrollToBottom() {
78
- await tick()
79
- if (chatAreaElement) {
80
- chatAreaElement.scrollTop = chatAreaElement.scrollHeight
81
- }
82
- }
83
-
84
- async function handleKeyDown(event: any) {
173
+ const handleKeyDown = async (event: KeyboardEvent) => {
85
174
  if (event.key === "Enter" && !event.shiftKey) {
86
175
  event.preventDefault()
87
- await prompt()
176
+ await sendMessage()
88
177
  }
89
178
  }
90
179
 
91
- async function prompt() {
92
- const resolvedChatAppId = await ensureChatApp()
180
+ const sendMessage = async () => {
181
+ const chatAppIdFromEnsure = await ensureChatApp()
93
182
 
94
183
  if (!chat) {
95
184
  chat = { title: "", messages: [], chatAppId: "", agentId: "" }
96
185
  }
97
186
 
98
- const chatAppId = chat.chatAppId || resolvedChatAppId
187
+ const chatAppId = chat.chatAppId || chatAppIdFromEnsure
99
188
  const agentId = chat.agentId
100
189
 
101
190
  if (!chatAppId) {
@@ -108,6 +197,8 @@
108
197
  return
109
198
  }
110
199
 
200
+ resolvedChatAppId = chatAppId
201
+
111
202
  if (
112
203
  persistConversation &&
113
204
  !chat._id &&
@@ -115,229 +206,163 @@
115
206
  ) {
116
207
  try {
117
208
  const newChat = await API.createChatConversation(
118
- {
119
- chatAppId,
120
- agentId,
121
- title: chat.title,
122
- },
209
+ { chatAppId, agentId, title: chat.title },
123
210
  workspaceId
124
211
  )
125
-
126
- chat = {
127
- ...chat,
128
- ...newChat,
129
- chatAppId,
130
- }
131
- } catch (err: any) {
212
+ chat = { ...chat, ...newChat, chatAppId }
213
+ resolvedConversationId = newChat._id
214
+ } catch (err: unknown) {
215
+ const errorMessage =
216
+ err instanceof Error
217
+ ? err.message
218
+ : "Could not start a new chat conversation"
132
219
  console.error(err)
133
- notifications.error(
134
- err?.message || "Could not start a new chat conversation"
135
- )
220
+ notifications.error(errorMessage)
136
221
  return
137
222
  }
223
+ } else if (chat._id) {
224
+ resolvedConversationId = chat._id
138
225
  }
139
226
 
140
- const userMessage: UIMessage<AgentMessageMetadata> = {
141
- id: uuidv4(),
142
- role: "user",
143
- parts: [{ type: "text", text: inputValue }],
144
- }
227
+ const text = inputValue.trim()
228
+ if (!text) return
145
229
 
146
- const updatedChat: ChatConversationLike = {
147
- ...chat,
148
- chatAppId: chat.chatAppId,
149
- transient: !persistConversation,
150
- messages: [...chat.messages, userMessage],
151
- }
230
+ inputValue = ""
231
+ chatInstance.sendMessage({ text })
232
+ }
152
233
 
153
- chat = updatedChat
154
- await scrollToBottom()
234
+ const toggleTool = (toolId: string) => {
235
+ expandedTools = { ...expandedTools, [toolId]: !expandedTools[toolId] }
236
+ }
155
237
 
156
- inputValue = ""
157
- loading = true
238
+ const formatToolOutput = (output: unknown): string =>
239
+ typeof output === "string" ? output : JSON.stringify(output, null, 2)
158
240
 
159
- try {
160
- const messageStream = await API.streamChatConversation(
161
- updatedChat as ChatConversationRequest,
162
- workspaceId
163
- )
164
-
165
- let streamedMessages = [...updatedChat.messages]
166
- let transientAssistantId = uuidv4()
167
-
168
- for await (const message of messageStream) {
169
- const normalizedMessage =
170
- !persistConversation && message?.role === "assistant" && !message?.id
171
- ? {
172
- ...message,
173
- id: transientAssistantId,
174
- }
175
- : message
176
-
177
- if (normalizedMessage?.id) {
178
- const existingIndex = streamedMessages.findIndex(
179
- existing => existing.id === normalizedMessage.id
180
- )
181
- if (existingIndex !== -1) {
182
- streamedMessages = streamedMessages.map((existing, index) =>
183
- index === existingIndex ? normalizedMessage : existing
184
- )
185
- } else {
186
- streamedMessages = [...streamedMessages, normalizedMessage]
187
- }
188
- } else if (normalizedMessage?.role === "assistant") {
189
- const lastIndex = [...streamedMessages]
190
- .reverse()
191
- .findIndex(existing => existing.role === "assistant")
192
- if (lastIndex !== -1) {
193
- const targetIndex = streamedMessages.length - 1 - lastIndex
194
- streamedMessages = streamedMessages.map((existing, index) =>
195
- index === targetIndex ? normalizedMessage : existing
196
- )
197
- } else {
198
- streamedMessages = [...streamedMessages, normalizedMessage]
199
- }
200
- } else {
201
- streamedMessages = [...streamedMessages, normalizedMessage]
202
- }
203
- chat = {
204
- ...updatedChat,
205
- messages: streamedMessages,
206
- }
207
- scrollToBottom()
208
- }
241
+ const getUserMessageText = (
242
+ message: UIMessage<AgentMessageMetadata>
243
+ ): string =>
244
+ message.parts
245
+ ?.filter(isTextUIPart)
246
+ .map(p => p.text)
247
+ .join("") || "[Empty message]"
209
248
 
210
- // When a chat is created for the first time the server generates the ID.
211
- // If we don't have it locally yet, retrieve the saved conversation so
212
- // subsequent prompts append to the same document instead of creating a new one.
213
- if (persistConversation && !chat._id && chat.chatAppId) {
214
- try {
215
- const history = await API.fetchChatHistory(chat.chatAppId)
216
- const lastMessageId = chat.messages[chat.messages.length - 1]?.id
217
- const savedConversation =
218
- history?.find(convo =>
219
- convo.messages.some(message => message.id === lastMessageId)
220
- ) || history?.[0]
221
- if (savedConversation) {
222
- chat = savedConversation
223
- }
224
- } catch (historyError) {
225
- console.error(historyError)
226
- }
227
- }
249
+ let mounted = $state(false)
228
250
 
229
- loading = false
230
- dispatch("chatSaved", { chatId: chat._id, chat })
231
- } catch (err: any) {
232
- console.error(err)
233
- notifications.error(err.message)
234
- loading = false
251
+ $effect(() => {
252
+ const currentChatId = chat?._id
253
+ if (currentChatId) {
254
+ resolvedConversationId = currentChatId
235
255
  }
256
+ })
236
257
 
237
- await tick()
238
- if (textareaElement) {
239
- textareaElement.focus()
258
+ $effect(() => {
259
+ if (!mounted) {
260
+ mounted = true
261
+ ensureChatApp()
262
+ tick().then(() => {
263
+ textareaElement?.focus()
264
+ })
240
265
  }
241
- }
266
+ })
242
267
 
243
- onMount(async () => {
244
- await ensureChatApp()
268
+ $effect(() => {
269
+ if (!chatAreaElement) return
245
270
 
246
- // Ensure we always autoscroll to reveal new messages
247
- observer = new MutationObserver(async () => {
248
- await tick()
249
- if (chatAreaElement) {
250
- chatAreaElement.scrollTop = chatAreaElement.scrollHeight
251
- }
271
+ const obs = new MutationObserver(scrollToBottom)
272
+ obs.observe(chatAreaElement, {
273
+ childList: true,
274
+ subtree: true,
275
+ attributes: true,
252
276
  })
253
277
 
254
- if (chatAreaElement) {
255
- observer.observe(chatAreaElement, {
256
- childList: true,
257
- subtree: true,
258
- attributes: true,
259
- })
260
- }
261
-
262
- await tick()
263
- if (textareaElement) {
264
- textareaElement.focus()
278
+ return () => {
279
+ obs.disconnect()
265
280
  }
266
281
  })
267
-
268
- $: {
269
- const currentId = chat?._id
270
- const isNewChat =
271
- !currentId && (!chat?.messages || chat.messages.length === 0)
272
- const shouldFocus =
273
- textareaElement &&
274
- ((currentId && currentId !== lastFocusedChatId) ||
275
- (isNewChat && chat && chat !== lastFocusedNewChat))
276
-
277
- if (shouldFocus) {
278
- tick().then(() => textareaElement?.focus())
279
- lastFocusedChatId = currentId
280
- lastFocusedNewChat = isNewChat ? chat : undefined
281
- }
282
- }
283
-
284
- onDestroy(() => {
285
- observer.disconnect()
286
- })
287
282
  </script>
288
283
 
289
284
  <div class="chat-area" bind:this={chatAreaElement}>
290
285
  <div class="chatbox">
291
- {#each chat.messages as message}
286
+ {#each messages as message (message.id)}
292
287
  {#if message.role === "user"}
293
288
  <div class="message user">
294
- <MarkdownViewer
295
- value={message.parts && message.parts.length > 0
296
- ? message.parts
297
- .filter(part => part.type === "text")
298
- .map(part => (part.type === "text" ? part.text : ""))
299
- .join("")
300
- : "[Empty message]"}
301
- />
289
+ <MarkdownViewer value={getUserMessageText(message)} />
302
290
  </div>
303
291
  {:else if message.role === "assistant"}
304
292
  <div class="message assistant">
305
- {#each message.parts || [] as part}
306
- {#if part.type === "text"}
307
- <MarkdownViewer value={part.text || ""} />
308
- {:else if part.type === "reasoning"}
293
+ {#each message.parts || [] as part, partIndex (partIndex)}
294
+ {#if isTextUIPart(part)}
295
+ <MarkdownViewer value={part.text} />
296
+ {:else if isReasoningUIPart(part)}
309
297
  <div class="reasoning-part">
310
298
  <div class="reasoning-label">Reasoning</div>
311
- <div class="reasoning-content">{part.text || ""}</div>
299
+ <div class="reasoning-content">{part.text}</div>
312
300
  </div>
313
- {:else if part.type?.startsWith("tool-") || part.type === "dynamic-tool"}
314
- {@const toolPart = part}
315
- <div class="tool-part">
316
- <div class="tool-header">
317
- <span class="tool-icon">🔧</span>
318
- <span class="tool-name"
319
- >{("toolName" in toolPart && toolPart.toolName) ||
320
- "Tool"}</span
301
+ {:else if isToolUIPart(part)}
302
+ {@const toolId = `${message.id}-${getToolName(part)}-${partIndex}`}
303
+ {@const isRunning =
304
+ part.state === "input-streaming" ||
305
+ part.state === "input-available"}
306
+ {@const isSuccess = part.state === "output-available"}
307
+ {@const isError = part.state === "output-error"}
308
+ <div class="tool-part" class:tool-running={isRunning}>
309
+ <button
310
+ class="tool-header"
311
+ type="button"
312
+ onclick={() => toggleTool(toolId)}
313
+ >
314
+ <span
315
+ class="tool-chevron"
316
+ class:expanded={expandedTools[toolId]}
321
317
  >
322
- {#if "state" in toolPart}
323
- {#if toolPart.state === "output-available"}
324
- <span class="tool-status success">✓</span>
325
- {:else if toolPart.state === "output-error"}
326
- <span class="tool-status error">✗</span>
327
- {:else if toolPart.state === "input-streaming"}
328
- <span class="tool-status pending">...</span>
329
- {:else if toolPart.state === "input-available"}
330
- <span class="tool-status pending">...</span>
318
+ <Icon name="caret-right" size="XS" />
319
+ </span>
320
+ <span class="tool-icon">
321
+ <Icon name="wrench" size="S" />
322
+ </span>
323
+ <span class="tool-name">{getToolName(part)}</span>
324
+ <span class="tool-status">
325
+ {#if isRunning}
326
+ <ProgressCircle size="S" />
327
+ {:else if isSuccess}
328
+ <Icon
329
+ name="check"
330
+ size="S"
331
+ color="var(--spectrum-global-color-green-600)"
332
+ />
333
+ {:else if isError}
334
+ <Icon
335
+ name="x"
336
+ size="S"
337
+ color="var(--spectrum-global-color-red-600)"
338
+ />
339
+ {/if}
340
+ </span>
341
+ </button>
342
+ {#if expandedTools[toolId]}
343
+ <div class="tool-details">
344
+ {#if part.input}
345
+ <div class="tool-section">
346
+ <div class="tool-section-label">Input</div>
347
+ <pre class="tool-section-content">{formatToolOutput(
348
+ part.input
349
+ )}</pre>
350
+ </div>
351
+ {/if}
352
+ {#if isSuccess && part.output}
353
+ <div class="tool-section">
354
+ <div class="tool-section-label">Output</div>
355
+ <pre class="tool-section-content">{formatToolOutput(
356
+ part.output
357
+ )}</pre>
358
+ </div>
359
+ {:else if isError && part.errorText}
360
+ <div class="tool-section tool-error">
361
+ <div class="tool-section-label">Error</div>
362
+ <pre
363
+ class="tool-section-content error-content">{part.errorText}</pre>
364
+ </div>
331
365
  {/if}
332
- {/if}
333
- </div>
334
- {#if "state" in toolPart && toolPart.state === "output-available" && "output" in toolPart && toolPart.output}
335
- <div class="tool-output">
336
- <div class="tool-output-label">Output:</div>
337
- <pre class="tool-output-content">{typeof toolPart.output ===
338
- "string"
339
- ? toolPart.output
340
- : JSON.stringify(toolPart.output, null, 2)}</pre>
341
366
  </div>
342
367
  {/if}
343
368
  </div>
@@ -367,7 +392,7 @@
367
392
  </div>
368
393
  {/if}
369
394
  {/each}
370
- {#if loading}
395
+ {#if isBusy}
371
396
  <div class="message system">
372
397
  <BBAI size="48px" animate />
373
398
  </div>
@@ -379,9 +404,9 @@
379
404
  bind:value={inputValue}
380
405
  bind:this={textareaElement}
381
406
  class="input spectrum-Textfield-input"
382
- on:keydown={handleKeyDown}
407
+ onkeydown={handleKeyDown}
383
408
  placeholder="Ask anything"
384
- disabled={loading}
409
+ disabled={isBusy}
385
410
  ></textarea>
386
411
  </div>
387
412
  </div>
@@ -480,19 +505,49 @@
480
505
  background-color: var(--grey-2);
481
506
  border: 1px solid var(--grey-3);
482
507
  border-radius: 8px;
508
+ transition: border-color 0.2s ease;
509
+ }
510
+
511
+ .tool-part.tool-running {
512
+ border-color: var(--spectrum-global-color-static-seafoam-600);
483
513
  }
484
514
 
485
515
  .tool-header {
486
516
  display: flex;
487
517
  align-items: center;
488
518
  gap: var(--spacing-s);
489
- margin-bottom: var(--spacing-s);
519
+ width: 100%;
520
+ padding: 0;
521
+ margin: 0;
522
+ background: none;
523
+ border: none;
524
+ cursor: pointer;
490
525
  font-weight: 600;
491
526
  font-size: 14px;
527
+ text-align: left;
528
+ }
529
+
530
+ .tool-header:hover {
531
+ opacity: 0.8;
532
+ }
533
+
534
+ .tool-chevron {
535
+ display: flex;
536
+ align-items: center;
537
+ justify-content: center;
538
+ transition: transform 0.15s ease;
539
+ color: var(--spectrum-global-color-gray-600);
540
+ }
541
+
542
+ .tool-chevron.expanded {
543
+ transform: rotate(90deg);
492
544
  }
493
545
 
494
546
  .tool-icon {
495
- font-size: 16px;
547
+ display: flex;
548
+ align-items: center;
549
+ justify-content: center;
550
+ color: var(--spectrum-global-color-static-seafoam-700);
496
551
  }
497
552
 
498
553
  .tool-name {
@@ -502,24 +557,33 @@
502
557
 
503
558
  .tool-status {
504
559
  margin-left: auto;
505
- font-size: 12px;
560
+ display: flex;
561
+ align-items: center;
562
+ justify-content: center;
506
563
  }
507
564
 
508
- .tool-status.success {
509
- color: var(--spectrum-global-color-green-600);
565
+ .tool-details {
566
+ margin-top: var(--spacing-m);
567
+ display: flex;
568
+ flex-direction: column;
569
+ gap: var(--spacing-s);
510
570
  }
511
571
 
512
- .tool-status.error {
513
- color: var(--spectrum-global-color-red-600);
572
+ .tool-section {
573
+ display: flex;
574
+ flex-direction: column;
575
+ gap: var(--spacing-xs);
514
576
  }
515
577
 
516
- .tool-status.pending {
578
+ .tool-section-label {
579
+ font-size: 11px;
580
+ font-weight: 600;
517
581
  color: var(--spectrum-global-color-gray-600);
582
+ text-transform: uppercase;
583
+ letter-spacing: 0.5px;
518
584
  }
519
585
 
520
- .tool-output,
521
- .tool-output-label,
522
- .tool-output-content {
586
+ .tool-section-content {
523
587
  background-color: var(--background);
524
588
  border: 1px solid var(--grey-3);
525
589
  border-radius: 4px;
@@ -529,6 +593,18 @@
529
593
  overflow-x: auto;
530
594
  white-space: pre-wrap;
531
595
  word-break: break-word;
596
+ margin: 0;
597
+ max-height: 200px;
598
+ overflow-y: auto;
599
+ }
600
+
601
+ .tool-error .tool-section-label {
602
+ color: var(--spectrum-global-color-red-600);
603
+ }
604
+
605
+ .error-content {
606
+ border-color: var(--spectrum-global-color-red-400);
607
+ color: var(--spectrum-global-color-red-700);
532
608
  }
533
609
 
534
610
  /* Reasoning parts styling */