@marimo-team/islands 0.15.3 → 0.15.5

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 (88) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DfvW3rBn.js → ConnectedDataExplorerComponent-CBeIYi8p.js} +2 -2
  2. package/dist/{ImageComparisonComponent-XaJshw7d.js → ImageComparisonComponent-Bk0a0xBq.js} +1 -1
  3. package/dist/{_baseUniq-dN9WKF9m.js → _baseUniq-utU5_Vu-.js} +1 -1
  4. package/dist/{any-language-editor-CpFniVi-.js → any-language-editor-PrUUh2lr.js} +1 -1
  5. package/dist/{architectureDiagram-W76B3OCA-Bpg85ZKv.js → architectureDiagram-W76B3OCA-D-vOp0UU.js} +4 -4
  6. package/dist/assets/{worker-Y-Q4G-N2.js → worker-BcG8m3h5.js} +3 -3
  7. package/dist/{blockDiagram-QIGZ2CNN-DS1kOHlW.js → blockDiagram-QIGZ2CNN-IG-z8q8A.js} +5 -5
  8. package/dist/{c4Diagram-FPNF74CW-CyRVKssw.js → c4Diagram-FPNF74CW-5AEXIX3t.js} +2 -2
  9. package/dist/{channel-BilGXox7.js → channel-ECVsTGGL.js} +1 -1
  10. package/dist/{chunk-4BX2VUAB-CZR39zCO.js → chunk-4BX2VUAB-DfJcd9e-.js} +1 -1
  11. package/dist/{chunk-55IACEB6-BIH-MYov.js → chunk-55IACEB6-BwT8MejR.js} +1 -1
  12. package/dist/{chunk-FMBD7UC4-4PZXFZE8.js → chunk-FMBD7UC4-DW7uxNR6.js} +1 -1
  13. package/dist/{chunk-K7UQS3LO-CEvWKznk.js → chunk-K7UQS3LO-BGn2ZPDQ.js} +4 -4
  14. package/dist/{chunk-QN33PNHL-D5LO5Jq_.js → chunk-QN33PNHL-BcIbOumv.js} +1 -1
  15. package/dist/{chunk-QZHKN3VN-6gwUonWI.js → chunk-QZHKN3VN-CMSnhk6x.js} +1 -1
  16. package/dist/{chunk-TVAH2DTR-3gm06QdU.js → chunk-TVAH2DTR-CZF2JRya.js} +3 -3
  17. package/dist/{chunk-TZMSLE5B-Cm8Iy9bO.js → chunk-TZMSLE5B-BHzN_BY6.js} +1 -1
  18. package/dist/{classDiagram-v2-RKCZMP56-DC529O_z.js → classDiagram-KNZD7YFC-2H7MseyB.js} +2 -2
  19. package/dist/{classDiagram-KNZD7YFC-DC529O_z.js → classDiagram-v2-RKCZMP56-2H7MseyB.js} +2 -2
  20. package/dist/{clone-CLoRX3j6.js → clone-DKQcSK7N.js} +1 -1
  21. package/dist/{cose-bilkent-S5V4N54A-qf5DlS6Y.js → cose-bilkent-S5V4N54A-CgvKFxTr.js} +2 -2
  22. package/dist/{dagre-5GWH7T2D-Ceocls0m.js → dagre-5GWH7T2D-VNFIipzt.js} +6 -6
  23. package/dist/{data-grid-overlay-editor-AqDS_UKe.js → data-grid-overlay-editor-XdqkKCVx.js} +2 -2
  24. package/dist/{diagram-N5W7TBWH-CP66oSiv.js → diagram-N5W7TBWH-D1s8h-eH.js} +5 -5
  25. package/dist/{diagram-QEK2KX5R-_YD4kxxi.js → diagram-QEK2KX5R-DOa-AstT.js} +3 -3
  26. package/dist/{diagram-S2PKOQOG-Cnj8T-OP.js → diagram-S2PKOQOG-CFZ-Y2zi.js} +3 -3
  27. package/dist/{dockerfile-Cm8cRYCN.js → dockerfile-zE-2DWBS.js} +1 -1
  28. package/dist/{erDiagram-AWTI2OKA-CGnvoHx6.js → erDiagram-AWTI2OKA-WxUYJfbS.js} +4 -4
  29. package/dist/{flowDiagram-PVAE7QVJ-DG-pr9R9.js → flowDiagram-PVAE7QVJ-dDZH2O1W.js} +5 -5
  30. package/dist/{ganttDiagram-OWAHRB6G-JmChtxvn.js → ganttDiagram-OWAHRB6G-D3CCqPQq.js} +4 -4
  31. package/dist/{gitGraphDiagram-NY62KEGX-D8wLfOPd.js → gitGraphDiagram-NY62KEGX-BHFylEwc.js} +4 -4
  32. package/dist/{glide-data-editor-9nC3iCIZ.js → glide-data-editor-D0aJSGV_.js} +3 -3
  33. package/dist/{graph-CoRe7vAN.js → graph-BPGEu6c8.js} +3 -3
  34. package/dist/{index-6qYeHHjQ.js → index-Bx2b23rX.js} +3 -3
  35. package/dist/{index-BthgsgYX.js → index-DotQhzoN.js} +1 -1
  36. package/dist/{index-jkm77Jrz.js → index-HtOEKQ3O.js} +1 -1
  37. package/dist/{index-BpzLh4Qe.js → index-eDB61tLS.js} +1 -1
  38. package/dist/{infoDiagram-STP46IZ2-BlXxvOrR.js → infoDiagram-STP46IZ2-DWhhqGPi.js} +2 -2
  39. package/dist/{journeyDiagram-BIP6EPQ6-CNRYs_Fc.js → journeyDiagram-BIP6EPQ6-CU8FpryL.js} +3 -3
  40. package/dist/{kanban-definition-6OIFK2YF-B9HeMAuP.js → kanban-definition-6OIFK2YF-CWhF_a4g.js} +2 -2
  41. package/dist/{layout-m2vOUiW1.js → layout-DGonEvAZ.js} +4 -4
  42. package/dist/{linear-DU6Q5CX3.js → linear-Cww2a6nQ.js} +1 -1
  43. package/dist/{main-BD2KGFpU.js → main-Bc0LY9fB.js} +20636 -20608
  44. package/dist/main.js +1 -1
  45. package/dist/{mermaid-HVCtvbyx.js → mermaid-DpJuOhRr.js} +30 -30
  46. package/dist/{min-DcGMA4e_.js → min-CFQjsG4L.js} +2 -2
  47. package/dist/{mindmap-definition-Q6HEUPPD-BW8UmIDQ.js → mindmap-definition-Q6HEUPPD-K513Ef1t.js} +3 -3
  48. package/dist/{number-overlay-editor-D8Hl0Syo.js → number-overlay-editor-DuSchUfE.js} +2 -2
  49. package/dist/{pieDiagram-ADFJNKIX-Bg-3zg5U.js → pieDiagram-ADFJNKIX-DAIIUJJO.js} +3 -3
  50. package/dist/{quadrantDiagram-LMRXKWRM-BO4IG6Yz.js → quadrantDiagram-LMRXKWRM-yuf-j7Os.js} +2 -2
  51. package/dist/{react-plotly-dkvHVuRb.js → react-plotly-B378DZ9U.js} +1 -1
  52. package/dist/{requirementDiagram-4UW4RH46-5sdTguSM.js → requirementDiagram-4UW4RH46-BBWvEl6q.js} +3 -3
  53. package/dist/{sankeyDiagram-GR3RE2ED-Buhlv9OI.js → sankeyDiagram-GR3RE2ED-B_TwV-dS.js} +1 -1
  54. package/dist/{sequenceDiagram-C3RYC4MD-C3qsM2UP.js → sequenceDiagram-C3RYC4MD-BVC6lltp.js} +3 -3
  55. package/dist/{slides-component-D209A0-s.js → slides-component-CPX3S0Y9.js} +1 -1
  56. package/dist/{stateDiagram-KXAO66HF-CopJ7G6P.js → stateDiagram-KXAO66HF-BCU1tYTD.js} +4 -4
  57. package/dist/{stateDiagram-v2-UMBNRL4Z-CejL8AKf.js → stateDiagram-v2-UMBNRL4Z-BdvN6wTu.js} +2 -2
  58. package/dist/style.css +1 -1
  59. package/dist/{time-BwSBitlN.js → time-CSIip6fV.js} +2 -2
  60. package/dist/{timeline-definition-XQNQX7LJ-DzMNTX-C.js → timeline-definition-XQNQX7LJ-CCxCPNQI.js} +1 -1
  61. package/dist/{treemap-75Q7IDZK-zeJG07dk.js → treemap-75Q7IDZK-Du6v0BzD.js} +5 -5
  62. package/dist/{vega-component-CUkiTayd.js → vega-component-Da93sTnp.js} +2 -2
  63. package/dist/{xychartDiagram-6GGTOJPD-DiENNXMS.js → xychartDiagram-6GGTOJPD-Oq6xaZKR.js} +2 -2
  64. package/package.json +6 -3
  65. package/src/components/ai/ai-provider-icon.tsx +5 -1
  66. package/src/components/chat/acp/__tests__/__snapshots__/prompt.test.ts.snap +304 -0
  67. package/src/components/chat/acp/__tests__/atoms.test.ts +56 -0
  68. package/src/components/chat/acp/__tests__/prompt.test.ts +12 -0
  69. package/src/components/chat/acp/__tests__/state.test.ts +621 -0
  70. package/src/components/chat/acp/agent-docs.tsx +78 -0
  71. package/src/components/chat/acp/agent-panel.css +23 -0
  72. package/src/components/chat/acp/agent-panel.tsx +715 -0
  73. package/src/components/chat/acp/agent-selector.tsx +138 -0
  74. package/src/components/chat/acp/blocks.tsx +664 -0
  75. package/src/components/chat/acp/common.tsx +198 -0
  76. package/src/components/chat/acp/prompt.ts +284 -0
  77. package/src/components/chat/acp/scroll-to-bottom-button.tsx +50 -0
  78. package/src/components/chat/acp/session-tabs.tsx +138 -0
  79. package/src/components/chat/acp/state.ts +263 -0
  80. package/src/components/chat/acp/thread.tsx +121 -0
  81. package/src/components/chat/acp/types.ts +63 -0
  82. package/src/components/chat/acp/utils.ts +45 -0
  83. package/src/components/chat/tool-call-accordion.tsx +1 -1
  84. package/src/components/editor/chrome/types.ts +10 -0
  85. package/src/components/editor/chrome/wrapper/app-chrome.tsx +17 -3
  86. package/src/core/config/feature-flag.tsx +2 -0
  87. package/src/plugins/impl/vega/vega.css +121 -0
  88. package/src/utils/Logger.ts +5 -6
@@ -0,0 +1,664 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+ import type {
3
+ ContentBlock,
4
+ ToolCallContent,
5
+ ToolCallLocation,
6
+ } from "@zed-industries/agent-client-protocol";
7
+ import { capitalize } from "lodash-es";
8
+ import {
9
+ FileAudio2Icon,
10
+ FileIcon,
11
+ FileImageIcon,
12
+ FileJsonIcon,
13
+ FileTextIcon,
14
+ FileVideoCameraIcon,
15
+ RotateCcwIcon,
16
+ WifiIcon,
17
+ WifiOffIcon,
18
+ WrenchIcon,
19
+ XCircleIcon,
20
+ XIcon,
21
+ } from "lucide-react";
22
+ import React from "react";
23
+ import { Streamdown } from "streamdown";
24
+ import { mergeToolCalls } from "use-acp";
25
+ import { JsonOutput } from "@/components/editor/output/JsonOutput";
26
+ import { Button } from "@/components/ui/button";
27
+ import {
28
+ Popover,
29
+ PopoverContent,
30
+ PopoverTrigger,
31
+ } from "@/components/ui/popover";
32
+ import { logNever } from "@/utils/assertNever";
33
+ import { cn } from "@/utils/cn";
34
+ import { type Base64String, base64ToDataURL } from "@/utils/json/base64";
35
+ import { Strings } from "@/utils/strings";
36
+ import { SimpleAccordion } from "./common";
37
+ import type {
38
+ AgentNotificationEvent,
39
+ AgentThoughtNotificationEvent,
40
+ ConnectionChangeNotificationEvent,
41
+ ContentBlockOf,
42
+ ErrorNotificationEvent,
43
+ PlanNotificationEvent,
44
+ SessionNotificationEventData,
45
+ ToolCallNotificationEvent,
46
+ ToolCallUpdateNotificationEvent,
47
+ UserNotificationEvent,
48
+ } from "./types";
49
+ import {
50
+ isAgentMessages,
51
+ isAgentThoughts,
52
+ isPlans,
53
+ isToolCalls,
54
+ isUserMessages,
55
+ } from "./utils";
56
+
57
+ export const ErrorBlock = (props: {
58
+ data: ErrorNotificationEvent["data"];
59
+ onRetry?: () => void;
60
+ onDismiss?: () => void;
61
+ }) => {
62
+ const { message } = props.data;
63
+
64
+ // Don't show WebSocket connection errors
65
+ if (message.includes("WebSocket")) {
66
+ return null;
67
+ }
68
+
69
+ return (
70
+ <div
71
+ className="border border-[var(--red-6)] bg-[var(--red-2)] rounded-lg p-4 my-2"
72
+ data-block-type="error"
73
+ >
74
+ <div className="flex items-start gap-3">
75
+ <div className="flex-shrink-0">
76
+ <XCircleIcon className="h-5 w-5 text-[var(--red-11)]" />
77
+ </div>
78
+ <div className="flex-1 min-w-0">
79
+ <div className="flex items-center gap-2 mb-2">
80
+ <h4 className="text-sm font-medium text-[var(--red-11)]">
81
+ Agent Error
82
+ </h4>
83
+ </div>
84
+ <div className="text-sm text-[var(--red-11)] leading-relaxed mb-3">
85
+ {message}
86
+ </div>
87
+ <div className="flex items-center gap-2">
88
+ {props.onRetry && (
89
+ <Button
90
+ size="xs"
91
+ variant="outline"
92
+ onClick={props.onRetry}
93
+ className="text-[var(--red-11)] border-[var(--red-6)] hover:bg-[var(--red-3)]"
94
+ >
95
+ <RotateCcwIcon className="h-3 w-3 mr-1" />
96
+ Retry
97
+ </Button>
98
+ )}
99
+ {props.onDismiss && (
100
+ <Button
101
+ size="xs"
102
+ variant="ghost"
103
+ onClick={props.onDismiss}
104
+ className="text-[var(--red-10)] hover:bg-[var(--red-3)]"
105
+ >
106
+ <XIcon className="h-3 w-3 mr-1" />
107
+ Dismiss
108
+ </Button>
109
+ )}
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ );
115
+ };
116
+
117
+ export const ConnectionChangeBlock = (props: {
118
+ data: ConnectionChangeNotificationEvent["data"];
119
+ isConnected: boolean;
120
+ onRetry?: () => void;
121
+ timestamp?: number;
122
+ }) => {
123
+ const { status } = props.data;
124
+
125
+ if (props.isConnected) {
126
+ return null;
127
+ }
128
+
129
+ const getStatusConfig = () => {
130
+ switch (status) {
131
+ case "connected":
132
+ return {
133
+ icon: <WifiIcon className="h-4 w-4" />,
134
+ title: "Connected to Agent",
135
+ message: "Successfully established connection with the AI agent",
136
+ bgColor: "bg-[var(--blue-2)]",
137
+ borderColor: "border-[var(--blue-6)]",
138
+ textColor: "text-[var(--blue-11)]",
139
+ iconColor: "text-[var(--blue-10)]",
140
+ };
141
+ case "disconnected":
142
+ return {
143
+ icon: <WifiOffIcon className="h-4 w-4" />,
144
+ title: "Disconnected from Agent",
145
+ message: "Connection to the AI agent has been lost",
146
+ bgColor: "bg-[var(--amber-2)]",
147
+ borderColor: "border-[var(--amber-6)]",
148
+ textColor: "text-[var(--amber-11)]",
149
+ iconColor: "text-[var(--amber-10)]",
150
+ };
151
+ case "connecting":
152
+ return {
153
+ icon: <WifiIcon className="h-4 w-4 animate-pulse" />,
154
+ title: "Connecting to Agent",
155
+ message: "Establishing connection with the AI agent...",
156
+ bgColor: "bg-[var(--gray-2)]",
157
+ borderColor: "border-[var(--gray-6)]",
158
+ textColor: "text-[var(--gray-11)]",
159
+ iconColor: "text-[var(--gray-10)]",
160
+ };
161
+ case "error":
162
+ return {
163
+ icon: <WifiOffIcon className="h-4 w-4" />,
164
+ title: "Connection Error",
165
+ message: "Failed to connect to the AI agent",
166
+ bgColor: "bg-[var(--red-2)]",
167
+ borderColor: "border-[var(--red-6)]",
168
+ textColor: "text-[var(--red-11)]",
169
+ iconColor: "text-[var(--red-10)]",
170
+ };
171
+ default:
172
+ return {
173
+ icon: <WifiOffIcon className="h-4 w-4" />,
174
+ title: "Connection Status Changed",
175
+ message: `Agent connection status: ${status}`,
176
+ bgColor: "bg-[var(--gray-2)]",
177
+ borderColor: "border-[var(--gray-6)]",
178
+ textColor: "text-[var(--gray-11)]",
179
+ iconColor: "text-[var(--gray-10)]",
180
+ };
181
+ }
182
+ };
183
+
184
+ const config = getStatusConfig();
185
+ const showRetry = status === "disconnected" || status === "error";
186
+
187
+ return (
188
+ <div
189
+ className={`border ${config.borderColor} ${config.bgColor} rounded-lg p-3 my-2`}
190
+ data-block-type="connection-change"
191
+ data-status={status}
192
+ >
193
+ <div className="flex items-start gap-3">
194
+ <div className={`flex-shrink-0 ${config.iconColor}`}>{config.icon}</div>
195
+ <div className="flex-1 min-w-0">
196
+ <div className="flex items-center gap-2 mb-1">
197
+ <h4 className={`text-sm font-medium ${config.textColor}`}>
198
+ {config.title}
199
+ </h4>
200
+ {props.timestamp && (
201
+ <span className="text-xs text-muted-foreground">
202
+ {new Date(props.timestamp).toLocaleTimeString()}
203
+ </span>
204
+ )}
205
+ </div>
206
+ <div className={`text-sm ${config.textColor} opacity-90 mb-2`}>
207
+ {config.message}
208
+ </div>
209
+ {showRetry && props.onRetry && (
210
+ <Button
211
+ size="xs"
212
+ variant="outline"
213
+ onClick={props.onRetry}
214
+ className={`${config.textColor} ${config.borderColor} hover:${config.bgColor}`}
215
+ >
216
+ <RotateCcwIcon className="h-3 w-3 mr-1" />
217
+ Retry Connection
218
+ </Button>
219
+ )}
220
+ </div>
221
+ </div>
222
+ </div>
223
+ );
224
+ };
225
+
226
+ export const AgentThoughtsBlock = (props: {
227
+ startTimestamp: number;
228
+ endTimestamp: number;
229
+ data: AgentThoughtNotificationEvent[];
230
+ }) => {
231
+ const startAsSeconds = props.startTimestamp / 1000;
232
+ const endAsSeconds = props.endTimestamp / 1000;
233
+ const totalSeconds = Math.round(endAsSeconds - startAsSeconds) || "1";
234
+ return (
235
+ <div className="text-xs text-muted-foreground">
236
+ <SimpleAccordion title={`Thought for ${totalSeconds}s`}>
237
+ <div className="flex flex-col gap-2 text-muted-foreground">
238
+ <ContentBlocks data={props.data.map((item) => item.content)} />
239
+ </div>
240
+ </SimpleAccordion>
241
+ </div>
242
+ );
243
+ };
244
+
245
+ export const PlansBlock = (props: { data: PlanNotificationEvent[] }) => {
246
+ const plans = props.data.flatMap((item) => item.entries);
247
+
248
+ return (
249
+ <div className="rounded-lg border bg-background p-2 text-xs">
250
+ <div className="flex items-center justify-between mb-2">
251
+ <span className="font-medium text-muted-foreground">
252
+ To-dos{" "}
253
+ <span className="font-normal text-muted-foreground">
254
+ {plans.length}
255
+ </span>
256
+ </span>
257
+ </div>
258
+ <ul className="flex flex-col gap-1">
259
+ {plans.map((item, index) => (
260
+ <li
261
+ key={`${item.status}-${index}`}
262
+ className={`
263
+ flex items-center gap-2 px-2 py-1 rounded
264
+ `}
265
+ >
266
+ <input
267
+ type="checkbox"
268
+ checked={item.status === "completed"}
269
+ readOnly={true}
270
+ className="accent-primary h-4 w-4 rounded border border-muted-foreground/30"
271
+ tabIndex={-1}
272
+ />
273
+ <span
274
+ className={cn(
275
+ "text-xs",
276
+ item.status === "completed" &&
277
+ "line-through text-muted-foreground",
278
+ )}
279
+ >
280
+ {item.content}
281
+ </span>
282
+ </li>
283
+ ))}
284
+ </ul>
285
+ </div>
286
+ );
287
+ };
288
+
289
+ export const UserMessagesBlock = (props: { data: UserNotificationEvent[] }) => {
290
+ return (
291
+ <div className="flex flex-col gap-2 text-muted-foreground border p-2 bg-background rounded break-words">
292
+ <ContentBlocks data={props.data.map((item) => item.content)} />
293
+ </div>
294
+ );
295
+ };
296
+
297
+ export const AgentMessagesBlock = (props: {
298
+ data: AgentNotificationEvent[];
299
+ }) => {
300
+ return (
301
+ <div className="flex flex-col gap-2">
302
+ <ContentBlocks data={props.data.map((item) => item.content)} />
303
+ </div>
304
+ );
305
+ };
306
+
307
+ export const ContentBlocks = (props: { data: ContentBlock[] }) => {
308
+ const renderBlock = (block: ContentBlock) => {
309
+ if (block.type === "text") {
310
+ return <Streamdown>{block.text}</Streamdown>;
311
+ }
312
+ if (block.type === "image") {
313
+ return <ImageBlock data={block} />;
314
+ }
315
+ if (block.type === "audio") {
316
+ return <AudioBlock data={block} />;
317
+ }
318
+ if (block.type === "resource") {
319
+ return <ResourceBlock data={block} />;
320
+ }
321
+ if (block.type === "resource_link") {
322
+ return <ResourceLinkBlock data={block} />;
323
+ }
324
+ logNever(block);
325
+ return null;
326
+ };
327
+
328
+ return (
329
+ <div>
330
+ {props.data.map((item, index) => {
331
+ return (
332
+ <React.Fragment key={`${item.type}-${index}`}>
333
+ {renderBlock(item)}
334
+ </React.Fragment>
335
+ );
336
+ })}
337
+ </div>
338
+ );
339
+ };
340
+
341
+ export const ImageBlock = (props: { data: ContentBlockOf<"image"> }) => {
342
+ return (
343
+ <img
344
+ src={`data:${props.data.mimeType};base64,${props.data.data}`}
345
+ alt={props.data.uri ?? ""}
346
+ />
347
+ );
348
+ };
349
+
350
+ export const AudioBlock = (props: { data: ContentBlockOf<"audio"> }) => {
351
+ return (
352
+ <audio
353
+ src={`data:${props.data.mimeType};base64,${props.data.data}`}
354
+ controls={true}
355
+ >
356
+ <track kind="captions" />
357
+ </audio>
358
+ );
359
+ };
360
+
361
+ export const ResourceBlock = (props: { data: ContentBlockOf<"resource"> }) => {
362
+ if ("text" in props.data.resource) {
363
+ return (
364
+ <Popover>
365
+ <PopoverTrigger>
366
+ <span className="flex items-center gap-1">
367
+ {props.data.resource.mimeType && (
368
+ <MimeIcon mimeType={props.data.resource.mimeType} />
369
+ )}
370
+ {props.data.resource.uri}
371
+ </span>
372
+ </PopoverTrigger>
373
+ <PopoverContent className="max-h-96 overflow-y-auto scrollbar-thin">
374
+ <Streamdown>{props.data.resource.text}</Streamdown>
375
+ </PopoverContent>
376
+ </Popover>
377
+ );
378
+ }
379
+
380
+ if ("blob" in props.data.resource) {
381
+ if (props.data.resource.mimeType?.startsWith("image/")) {
382
+ return (
383
+ <ImageBlock
384
+ data={{
385
+ type: "image",
386
+ mimeType: props.data.resource.mimeType,
387
+ data: props.data.resource.blob,
388
+ }}
389
+ />
390
+ );
391
+ }
392
+ if (props.data.resource.mimeType?.startsWith("audio/")) {
393
+ return (
394
+ <AudioBlock
395
+ data={{
396
+ type: "audio",
397
+ mimeType: props.data.resource.mimeType,
398
+ data: props.data.resource.blob,
399
+ }}
400
+ />
401
+ );
402
+ }
403
+ const dataURL = base64ToDataURL(
404
+ props.data.resource.blob as Base64String,
405
+ props.data.resource.mimeType ?? "",
406
+ );
407
+ return (
408
+ <a href={dataURL} className="flex items-center gap-1" download={true}>
409
+ {props.data.resource.mimeType && (
410
+ <MimeIcon mimeType={props.data.resource.mimeType} />
411
+ )}
412
+ {props.data.resource.uri}
413
+ </a>
414
+ );
415
+ }
416
+ logNever(props.data.resource);
417
+ return null;
418
+ };
419
+
420
+ export const ResourceLinkBlock = (props: {
421
+ data: ContentBlockOf<"resource_link">;
422
+ }) => {
423
+ if (props.data.uri.startsWith("http")) {
424
+ return (
425
+ <a
426
+ href={props.data.uri}
427
+ target="_blank"
428
+ rel="noopener noreferrer"
429
+ className="text-link hover:underline"
430
+ >
431
+ {props.data.name}
432
+ </a>
433
+ );
434
+ }
435
+
436
+ return (
437
+ <span className="flex items-center gap-1">
438
+ {props.data.mimeType && <MimeIcon mimeType={props.data.mimeType} />}
439
+ {props.data.name || props.data.title || props.data.uri}
440
+ </span>
441
+ );
442
+ };
443
+
444
+ export const MimeIcon = (props: { mimeType: string }) => {
445
+ if (props.mimeType.startsWith("image/")) {
446
+ return <FileImageIcon className="h-2 w-2" />;
447
+ }
448
+ if (props.mimeType.startsWith("audio/")) {
449
+ return <FileAudio2Icon className="h-2 w-2" />;
450
+ }
451
+ if (props.mimeType.startsWith("video/")) {
452
+ return <FileVideoCameraIcon className="h-2 w-2" />;
453
+ }
454
+ if (props.mimeType.startsWith("text/")) {
455
+ return <FileTextIcon className="h-2 w-2" />;
456
+ }
457
+ if (props.mimeType.startsWith("application/")) {
458
+ return <FileJsonIcon className="h-2 w-2" />;
459
+ }
460
+ return <FileIcon className="h-2 w-2" />;
461
+ };
462
+
463
+ export const SessionNotificationsBlock = <
464
+ T extends SessionNotificationEventData,
465
+ >(props: {
466
+ data: T[];
467
+ startTimestamp: number;
468
+ endTimestamp: number;
469
+ }) => {
470
+ if (props.data.length === 0) {
471
+ return null;
472
+ }
473
+ const kind = props.data[0].sessionUpdate;
474
+
475
+ const renderItems = (items: T[]) => {
476
+ if (isToolCalls(items)) {
477
+ return <ToolNotificationsBlock data={items} />;
478
+ }
479
+ if (isAgentThoughts(items)) {
480
+ return (
481
+ <AgentThoughtsBlock
482
+ startTimestamp={props.startTimestamp}
483
+ endTimestamp={props.endTimestamp}
484
+ data={items}
485
+ />
486
+ );
487
+ }
488
+ if (isUserMessages(items)) {
489
+ return <UserMessagesBlock data={items} />;
490
+ }
491
+ if (isAgentMessages(items)) {
492
+ return <AgentMessagesBlock data={items} />;
493
+ }
494
+ if (isPlans(items)) {
495
+ return <PlansBlock data={items} />;
496
+ }
497
+
498
+ return (
499
+ <SimpleAccordion title={items[0].sessionUpdate}>
500
+ <JsonOutput data={items} format="tree" className="max-h-64" />
501
+ </SimpleAccordion>
502
+ );
503
+ };
504
+
505
+ return (
506
+ <div className="flex flex-col text-sm gap-2" data-block-type={kind}>
507
+ {renderItems(props.data)}
508
+ </div>
509
+ );
510
+ };
511
+
512
+ export const ToolNotificationsBlock = (props: {
513
+ data: Array<ToolCallNotificationEvent | ToolCallUpdateNotificationEvent>;
514
+ }) => {
515
+ const toolCalls = mergeToolCalls(props.data);
516
+
517
+ return (
518
+ <div className="flex flex-col text-muted-foreground">
519
+ {toolCalls.map((item) => (
520
+ <SimpleAccordion
521
+ key={item.toolCallId}
522
+ status={
523
+ item.status === "completed"
524
+ ? "success"
525
+ : item.status === "failed"
526
+ ? "error"
527
+ : item.status === "in_progress" || item.status === "pending"
528
+ ? "loading"
529
+ : undefined
530
+ }
531
+ title={
532
+ <span data-tool-data={JSON.stringify(item)}>
533
+ {handleBackticks(toolTitle(item))}
534
+ </span>
535
+ }
536
+ defaultIcon={<WrenchIcon className="h-3 w-3" />}
537
+ >
538
+ <ToolBodyBlock data={item} />
539
+ </SimpleAccordion>
540
+ ))}
541
+ </div>
542
+ );
543
+ };
544
+
545
+ export const DiffBlocks = (props: {
546
+ data: Array<Extract<ToolCallContent, { type: "diff" }>>;
547
+ }) => {
548
+ return (
549
+ <div className="flex flex-col gap-2 text-muted-foreground">
550
+ {props.data.map((item) => {
551
+ return (
552
+ <div
553
+ key={item.path}
554
+ className="border rounded-md overflow-hidden bg-[var(--gray-2)] max-h-64 overflow-y-auto scrollbar-thin"
555
+ >
556
+ {/* File path header */}
557
+ <div className="px-2 py-1 bg-[var(--gray-2)] border-b text-xs font-medium text-[var(--gray-11)]">
558
+ {item.path}
559
+ </div>
560
+
561
+ <div className="font-mono text-xs">
562
+ {/* Removed lines */}
563
+ {item.oldText && (
564
+ <div className="px-3 py-1 border-b bg-[var(--red-2)] border-b-[var(--red-6)]">
565
+ <div className="flex">
566
+ <span className="text-[var(--red-11)] select-none mr-2">
567
+ -
568
+ </span>
569
+ <pre className="text-[var(--red-11)] px-1 rounded whitespace-pre-wrap break-words flex-1 line-through opacity-80">
570
+ {item.oldText}
571
+ </pre>
572
+ </div>
573
+ </div>
574
+ )}
575
+
576
+ {/* Added lines */}
577
+ {item.newText && (
578
+ <div className="px-3 py-1 bg-[var(--grass-2)] border-b-[var(--grass-6)]">
579
+ <div className="flex">
580
+ <span className="text-[var(--grass-11)] select-none mr-2">
581
+ +
582
+ </span>
583
+ <pre className="text-[var(--grass-11)] px-1 rounded whitespace-pre-wrap break-words flex-1 italic opacity-90">
584
+ {item.newText}
585
+ </pre>
586
+ </div>
587
+ </div>
588
+ )}
589
+ </div>
590
+ </div>
591
+ );
592
+ })}
593
+ </div>
594
+ );
595
+ };
596
+
597
+ function toolTitle(
598
+ item: Pick<ToolCallUpdateNotificationEvent, "title" | "kind" | "locations">,
599
+ ) {
600
+ const prefix =
601
+ item.title || Strings.startCase(item.kind || "") || "Tool call";
602
+ const firstLocation = item.locations?.[0];
603
+ // Add the first location if it is not in the title already
604
+ if (firstLocation && !prefix.includes(firstLocation.path)) {
605
+ return `${prefix}: ${firstLocation.path}`;
606
+ }
607
+ return prefix;
608
+ }
609
+
610
+ function handleBackticks(text: string) {
611
+ if (text.startsWith("`") && text.endsWith("`")) {
612
+ return <i>{text.slice(1, -1)}</i>;
613
+ }
614
+ return text;
615
+ }
616
+
617
+ export const LocationsBlock = (props: { data: ToolCallLocation[] }) => {
618
+ // Only show locations if there are multiple locations, otherwise it is
619
+ // in the title
620
+ if (props.data.length <= 1) {
621
+ return null;
622
+ }
623
+
624
+ const locations = props.data.map((item) => {
625
+ if (item.line) {
626
+ return `${item.path}:${item.line}`;
627
+ }
628
+ return item.path;
629
+ });
630
+ return <div className="flex flex-col gap-2">{locations.join("\n")}</div>;
631
+ };
632
+
633
+ export const ToolBodyBlock = (props: {
634
+ data:
635
+ | Omit<ToolCallNotificationEvent, "sessionUpdate">
636
+ | Omit<ToolCallUpdateNotificationEvent, "sessionUpdate">;
637
+ }) => {
638
+ const content = props.data.content
639
+ ?.filter((item) => item.type === "content")
640
+ .map((item) => item.content);
641
+ const diffs = props.data.content?.filter((item) => item.type === "diff");
642
+ const locations = props.data.locations;
643
+ const isFailed = props.data.status === "failed";
644
+ const hasLocations = locations && locations.length > 0;
645
+
646
+ if (content?.length === 0 && diffs?.length === 0 && hasLocations) {
647
+ return (
648
+ <div className="flex flex-col gap-2 pr-2">
649
+ <span className="text-xs text-muted-foreground">
650
+ {capitalize(props.data.kind || "")}{" "}
651
+ {locations?.map((item) => item.path).join(", ")}
652
+ </span>
653
+ </div>
654
+ );
655
+ }
656
+
657
+ return (
658
+ <div className="flex flex-col gap-2 pr-2">
659
+ {locations && <LocationsBlock data={locations} />}
660
+ {content && <ContentBlocks data={content} />}
661
+ {diffs && !isFailed && <DiffBlocks data={diffs} />}
662
+ </div>
663
+ );
664
+ };