@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,715 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { useAtom } from "jotai";
4
+ import { capitalize } from "lodash-es";
5
+ import {
6
+ BotMessageSquareIcon,
7
+ RefreshCwIcon,
8
+ StopCircleIcon,
9
+ } from "lucide-react";
10
+ import React, { memo, useEffect, useRef, useState } from "react";
11
+ import useEvent from "react-use-event-hook";
12
+ import { useAcpClient } from "use-acp";
13
+ import {
14
+ ConnectionStatus,
15
+ PermissionRequest,
16
+ } from "@/components/chat/acp/common";
17
+ import { PromptInput } from "@/components/editor/ai/add-cell-with-ai";
18
+ import { PanelEmptyState } from "@/components/editor/chrome/panels/empty-state";
19
+ import { Spinner } from "@/components/icons/spinner";
20
+ import { Button } from "@/components/ui/button";
21
+ import { cn } from "@/utils/cn";
22
+ import { Logger } from "@/utils/Logger";
23
+ import { AgentDocs } from "./agent-docs";
24
+ import { AgentSelector } from "./agent-selector";
25
+ import ScrollToBottomButton from "./scroll-to-bottom-button";
26
+ import { SessionTabs } from "./session-tabs";
27
+ import {
28
+ agentSessionStateAtom,
29
+ type ExternalAgentId,
30
+ getAgentWebSocketUrl,
31
+ selectedTabAtom,
32
+ updateSessionExternalAgentSessionId,
33
+ updateSessionTitle,
34
+ } from "./state";
35
+ import { AgentThread } from "./thread";
36
+ import "./agent-panel.css";
37
+ import type {
38
+ ReadTextFileResponse,
39
+ RequestPermissionResponse,
40
+ WriteTextFileResponse,
41
+ } from "@zed-industries/agent-client-protocol";
42
+ import { toast } from "@/components/ui/use-toast";
43
+ import { useRequestClient } from "@/core/network/requests";
44
+ import { filenameAtom } from "@/core/saving/file-state";
45
+ import { store } from "@/core/state/jotai";
46
+ import { Functions } from "@/utils/functions";
47
+ import { Paths } from "@/utils/paths";
48
+ import { getAgentPrompt } from "./prompt";
49
+ import type {
50
+ AgentConnectionState,
51
+ AgentPendingPermission,
52
+ ExternalAgentSessionId,
53
+ NotificationEvent,
54
+ } from "./types";
55
+
56
+ const logger = Logger.get("agents");
57
+
58
+ interface AgentTitleProps {
59
+ currentAgentId?: ExternalAgentId;
60
+ }
61
+
62
+ const AgentTitle = memo<AgentTitleProps>(({ currentAgentId }) => (
63
+ <span className="text-sm font-medium">
64
+ {currentAgentId ? capitalize(currentAgentId) : "Agents"}
65
+ </span>
66
+ ));
67
+ AgentTitle.displayName = "AgentTitle";
68
+
69
+ interface ConnectionControlProps {
70
+ connectionState: AgentConnectionState;
71
+ onConnect: () => void;
72
+ onDisconnect: () => void;
73
+ }
74
+
75
+ const ConnectionControl = memo<ConnectionControlProps>(
76
+ ({ connectionState, onConnect, onDisconnect }) => {
77
+ const isConnected = connectionState.status === "connected";
78
+
79
+ return (
80
+ <Button
81
+ variant="outline"
82
+ size="xs"
83
+ onClick={isConnected ? onDisconnect : onConnect}
84
+ disabled={connectionState.status === "connecting"}
85
+ >
86
+ {isConnected ? "Disconnect" : "Connect"}
87
+ </Button>
88
+ );
89
+ },
90
+ );
91
+ ConnectionControl.displayName = "ConnectionControl";
92
+
93
+ interface HeaderInfoProps {
94
+ currentAgentId?: ExternalAgentId;
95
+ connectionStatus: string;
96
+ shouldShowConnectionControl?: boolean;
97
+ }
98
+
99
+ const HeaderInfo = memo<HeaderInfoProps>(
100
+ ({ currentAgentId, connectionStatus, shouldShowConnectionControl }) => (
101
+ <div className="flex items-center gap-2">
102
+ <BotMessageSquareIcon className="h-4 w-4 text-muted-foreground" />
103
+ <AgentTitle currentAgentId={currentAgentId} />
104
+ {shouldShowConnectionControl && (
105
+ <ConnectionStatus status={connectionStatus} />
106
+ )}
107
+ </div>
108
+ ),
109
+ );
110
+ HeaderInfo.displayName = "HeaderInfo";
111
+
112
+ interface AgentPanelHeaderProps {
113
+ connectionState: AgentConnectionState;
114
+ currentAgentId?: ExternalAgentId;
115
+ onConnect: () => void;
116
+ onDisconnect: () => void;
117
+ onRestartThread?: () => void;
118
+ hasActiveSession?: boolean;
119
+ shouldShowConnectionControl?: boolean;
120
+ }
121
+
122
+ const AgentPanelHeader = memo<AgentPanelHeaderProps>(
123
+ ({
124
+ connectionState,
125
+ currentAgentId,
126
+ onConnect,
127
+ onDisconnect,
128
+ onRestartThread,
129
+ hasActiveSession,
130
+ shouldShowConnectionControl,
131
+ }) => (
132
+ <div className="flex border-b px-3 py-2 justify-between shrink-0 items-center">
133
+ <HeaderInfo
134
+ currentAgentId={currentAgentId}
135
+ connectionStatus={connectionState.status}
136
+ shouldShowConnectionControl={shouldShowConnectionControl}
137
+ />
138
+ <div className="flex items-center gap-2">
139
+ {hasActiveSession &&
140
+ connectionState.status === "connected" &&
141
+ onRestartThread && (
142
+ <Button
143
+ variant="outline"
144
+ size="xs"
145
+ onClick={onRestartThread}
146
+ title="Restart thread (create new session)"
147
+ >
148
+ <RefreshCwIcon className="h-3 w-3 mr-1" />
149
+ Restart
150
+ </Button>
151
+ )}
152
+
153
+ {shouldShowConnectionControl && (
154
+ <ConnectionControl
155
+ connectionState={connectionState}
156
+ onConnect={onConnect}
157
+ onDisconnect={onDisconnect}
158
+ />
159
+ )}
160
+ </div>
161
+ </div>
162
+ ),
163
+ );
164
+ AgentPanelHeader.displayName = "AgentPanelHeader";
165
+
166
+ interface EmptyStateProps {
167
+ currentAgentId?: ExternalAgentId;
168
+ connectionState: AgentConnectionState;
169
+ onConnect: () => void;
170
+ onDisconnect: () => void;
171
+ }
172
+
173
+ const EmptyState = memo<EmptyStateProps>(
174
+ ({ currentAgentId, connectionState, onConnect, onDisconnect }) => (
175
+ <div className="flex flex-col h-full">
176
+ <AgentPanelHeader
177
+ connectionState={connectionState}
178
+ currentAgentId={currentAgentId}
179
+ onConnect={onConnect}
180
+ onDisconnect={onDisconnect}
181
+ hasActiveSession={false}
182
+ />
183
+ <SessionTabs />
184
+ <div className="flex-1 flex items-center justify-center p-6">
185
+ <div className="max-w-md w-full space-y-6">
186
+ <PanelEmptyState
187
+ title="No Agent Sessions"
188
+ description="Create a new session to start a conversation"
189
+ action={<AgentSelector className="border-y-1 rounded" />}
190
+ icon={<BotMessageSquareIcon />}
191
+ />
192
+ {connectionState.status === "disconnected" && (
193
+ <AgentDocs
194
+ className="border-t pt-6"
195
+ title="Connect to an agent"
196
+ description="Start agents by running these commands in your terminal:"
197
+ />
198
+ )}
199
+ </div>
200
+ </div>
201
+ </div>
202
+ ),
203
+ );
204
+ EmptyState.displayName = "EmptyState";
205
+
206
+ interface LoadingIndicatorProps {
207
+ isLoading: boolean;
208
+ isRequestingPermission: boolean;
209
+ onStop: () => void;
210
+ }
211
+
212
+ const LoadingIndicator = memo<LoadingIndicatorProps>(
213
+ ({ isLoading, isRequestingPermission, onStop }) => {
214
+ if (!isLoading) {
215
+ return null;
216
+ }
217
+
218
+ return (
219
+ <div className="px-3 py-2 border-t bg-muted/30 flex-shrink-0">
220
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
221
+ <div className="flex items-center gap-2">
222
+ <Spinner size="small" className="text-primary" />
223
+ {isRequestingPermission ? (
224
+ <span>Waiting for permission to continue...</span>
225
+ ) : (
226
+ <span>Agent is working...</span>
227
+ )}
228
+ </div>
229
+ <div className="flex items-center gap-2">
230
+ <Button
231
+ size="sm"
232
+ variant="outline"
233
+ onClick={onStop}
234
+ className="h-6 px-2"
235
+ >
236
+ <StopCircleIcon className="h-3 w-3 mr-1" />
237
+ <span className="text-xs">Stop</span>
238
+ </Button>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ );
243
+ },
244
+ );
245
+ LoadingIndicator.displayName = "LoadingIndicator";
246
+
247
+ interface PromptAreaProps {
248
+ isLoading: boolean;
249
+ activeSessionId: ExternalAgentSessionId | null;
250
+ promptValue: string;
251
+ onPromptValueChange: (value: string) => void;
252
+ onPromptSubmit: (e: KeyboardEvent | undefined, prompt: string) => void;
253
+ }
254
+
255
+ const PromptArea = memo<PromptAreaProps>(
256
+ ({
257
+ isLoading,
258
+ activeSessionId,
259
+ promptValue,
260
+ onPromptValueChange,
261
+ onPromptSubmit,
262
+ }) => (
263
+ <div
264
+ className={cn(
265
+ "px-3 py-2 border-t bg-background flex-shrink-0 min-h-[80px]",
266
+ (isLoading || !activeSessionId) && "opacity-50 pointer-events-none",
267
+ )}
268
+ >
269
+ <PromptInput
270
+ value={promptValue}
271
+ onChange={isLoading ? Functions.NOOP : onPromptValueChange}
272
+ onSubmit={onPromptSubmit}
273
+ onClose={Functions.NOOP}
274
+ placeholder={isLoading ? "Processing..." : "Ask your AI agent..."}
275
+ className={isLoading ? "opacity-50 pointer-events-none" : ""}
276
+ maxHeight="120px"
277
+ />
278
+ </div>
279
+ ),
280
+ );
281
+ PromptArea.displayName = "PromptArea";
282
+
283
+ interface ChatContentProps {
284
+ hasNotifications: boolean;
285
+ connectionState: AgentConnectionState;
286
+ sessionId: ExternalAgentSessionId | null;
287
+ notifications: NotificationEvent[];
288
+ pendingPermission: AgentPendingPermission;
289
+ onResolvePermission: (option: unknown) => void;
290
+ onRetryConnection?: () => void;
291
+ onRetryLastAction?: () => void;
292
+ onDismissError?: (errorId: string) => void;
293
+ }
294
+
295
+ const ChatContent = memo<ChatContentProps>(
296
+ ({
297
+ hasNotifications,
298
+ connectionState,
299
+ notifications,
300
+ pendingPermission,
301
+ onResolvePermission,
302
+ onRetryConnection,
303
+ onRetryLastAction,
304
+ onDismissError,
305
+ sessionId,
306
+ }) => {
307
+ const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
308
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
309
+
310
+ // Scroll handler to determine if we're at the bottom of the chat
311
+ const handleScroll = useEvent(() => {
312
+ const container = scrollContainerRef.current;
313
+ if (!container) {
314
+ return;
315
+ }
316
+
317
+ const { scrollTop, scrollHeight, clientHeight } = container;
318
+ const hasOverflow = scrollHeight > clientHeight;
319
+ const isAtBottom = hasOverflow
320
+ ? Math.abs(scrollHeight - clientHeight - scrollTop) < 5
321
+ : true; // 5px threshold
322
+ setIsScrolledToBottom(isAtBottom);
323
+ });
324
+
325
+ const scrollToBottom = useEvent(() => {
326
+ const container = scrollContainerRef.current;
327
+ if (!container) {
328
+ return;
329
+ }
330
+
331
+ container.scrollTo({
332
+ top: container.scrollHeight,
333
+ behavior: "smooth",
334
+ });
335
+ });
336
+
337
+ // Auto-scroll to bottom when new notifications arrive (if already at bottom)
338
+ useEffect(() => {
339
+ if (isScrolledToBottom && notifications.length > 0) {
340
+ // Use setTimeout to ensure DOM is updated before scrolling
341
+ const timeout = setTimeout(scrollToBottom, 100);
342
+ return () => clearTimeout(timeout);
343
+ }
344
+ }, [notifications.length, isScrolledToBottom, scrollToBottom]);
345
+
346
+ return (
347
+ <div className="flex-1 flex flex-col overflow-hidden flex-shrink-0 relative">
348
+ {pendingPermission && (
349
+ <div className="p-3 border-b">
350
+ <PermissionRequest
351
+ permission={pendingPermission}
352
+ onResolve={onResolvePermission}
353
+ />
354
+ </div>
355
+ )}
356
+
357
+ <div
358
+ ref={scrollContainerRef}
359
+ className="flex-1 bg-muted/20 w-full flex flex-col overflow-y-auto p-2"
360
+ onScroll={handleScroll}
361
+ >
362
+ {sessionId && (
363
+ <div className="text-xs text-muted-foreground mb-2 px-2">
364
+ Session ID: {sessionId}
365
+ </div>
366
+ )}
367
+ {hasNotifications ? (
368
+ <div className="space-y-2">
369
+ <AgentThread
370
+ isConnected={connectionState.status === "connected"}
371
+ notifications={notifications}
372
+ onRetryConnection={onRetryConnection}
373
+ onRetryLastAction={onRetryLastAction}
374
+ onDismissError={onDismissError}
375
+ />
376
+ </div>
377
+ ) : (
378
+ <div className="flex items-center justify-center h-full min-h-[200px]">
379
+ <PanelEmptyState
380
+ title="Waiting for agent"
381
+ description="Your AI agent will appear here when active"
382
+ icon={<BotMessageSquareIcon />}
383
+ />
384
+ </div>
385
+ )}
386
+ </div>
387
+
388
+ <ScrollToBottomButton
389
+ isVisible={!isScrolledToBottom && hasNotifications}
390
+ onScrollToBottom={scrollToBottom}
391
+ />
392
+ </div>
393
+ );
394
+ },
395
+ );
396
+ ChatContent.displayName = "ChatContent";
397
+
398
+ const NO_WS_SET = "_skip_auto_connect_";
399
+
400
+ function getCwd() {
401
+ const filename = store.get(filenameAtom);
402
+ if (!filename) {
403
+ return "";
404
+ }
405
+ return Paths.dirname(filename);
406
+ }
407
+
408
+ const AgentPanel: React.FC = () => {
409
+ const [isLoading, setIsLoading] = useState(false);
410
+ const [promptValue, setPromptValue] = useState("");
411
+
412
+ const [selectedTab] = useAtom(selectedTabAtom);
413
+ const [sessionState, setSessionState] = useAtom(agentSessionStateAtom);
414
+
415
+ const wsUrl = selectedTab
416
+ ? getAgentWebSocketUrl(selectedTab.agentId)
417
+ : NO_WS_SET;
418
+ const { sendUpdateFile, sendFileDetails } = useRequestClient();
419
+ const isCreatingNewSession = useRef(false);
420
+
421
+ const acpClient = useAcpClient({
422
+ wsUrl,
423
+ clientOptions: {
424
+ readTextFile: (request): Promise<ReadTextFileResponse> => {
425
+ logger.debug("Agent requesting file read", {
426
+ path: request.path,
427
+ });
428
+ return sendFileDetails({ path: request.path }).then((response) => ({
429
+ content: response.contents || "",
430
+ }));
431
+ },
432
+ writeTextFile: (request): Promise<WriteTextFileResponse> => {
433
+ logger.debug("Agent requesting file write", {
434
+ path: request.path,
435
+ contentLength: request.content.length,
436
+ });
437
+ return sendUpdateFile({
438
+ path: request.path,
439
+ contents: request.content,
440
+ }).then(() => null);
441
+ },
442
+ },
443
+ autoConnect: false, // We'll manage connection manually based on active session
444
+ });
445
+
446
+ const {
447
+ connect,
448
+ disconnect,
449
+ connectionState,
450
+ notifications,
451
+ pendingPermission,
452
+ resolvePermission,
453
+ activeSessionId,
454
+ agent,
455
+ } = acpClient;
456
+
457
+ // Auto-connect to agent when we have an active session, but only once per session
458
+ useEffect(() => {
459
+ if (wsUrl === NO_WS_SET) {
460
+ return;
461
+ }
462
+
463
+ logger.debug("Auto-connecting to agent", {
464
+ sessionId: activeSessionId,
465
+ });
466
+ connect();
467
+
468
+ return () => {
469
+ // We don't want to disconnect so users can switch between different
470
+ // panels without losing their session
471
+ };
472
+ // eslint-disable-next-line react-hooks/exhaustive-deps
473
+ }, [wsUrl]);
474
+
475
+ const handleNewSession = useEvent(async () => {
476
+ if (isCreatingNewSession.current) {
477
+ return;
478
+ }
479
+ if (!agent) {
480
+ return;
481
+ }
482
+
483
+ // If there is an active session, we should stop it
484
+ if (activeSessionId) {
485
+ await agent.cancel({ sessionId: activeSessionId }).catch((error) => {
486
+ logger.error("Failed to cancel active session", { error });
487
+ });
488
+ }
489
+
490
+ logger.debug("Creating new agent session", {});
491
+ isCreatingNewSession.current = true;
492
+ const newSession = await agent
493
+ .newSession({
494
+ cwd: getCwd(),
495
+ mcpServers: [],
496
+ })
497
+ .finally(() => {
498
+ isCreatingNewSession.current = false;
499
+ });
500
+ setSessionState((prev) =>
501
+ updateSessionExternalAgentSessionId(
502
+ prev,
503
+ newSession.sessionId as ExternalAgentSessionId,
504
+ ),
505
+ );
506
+ });
507
+
508
+ const handleResumeSession = useEvent(
509
+ async (previousSessionId: ExternalAgentSessionId) => {
510
+ if (!agent) {
511
+ return;
512
+ }
513
+ logger.debug("Resuming agent session", {
514
+ sessionId: previousSessionId,
515
+ });
516
+ if (!agent.loadSession) {
517
+ throw new Error("Agent does not support loading sessions");
518
+ }
519
+ await agent.loadSession({
520
+ sessionId: previousSessionId,
521
+ cwd: getCwd(),
522
+ mcpServers: [],
523
+ });
524
+ setSessionState((prev) =>
525
+ updateSessionExternalAgentSessionId(prev, previousSessionId),
526
+ );
527
+ },
528
+ );
529
+
530
+ // Create or resume a session when successfully connected
531
+ const isConnected = connectionState.status === "connected";
532
+ const tabLastActiveSessionId = selectedTab?.externalAgentSessionId;
533
+ useEffect(() => {
534
+ // No need to do anything if we're not connected, don't have an agent, or don't have a selected tab
535
+ if (!isConnected || !selectedTab || !agent) {
536
+ return;
537
+ }
538
+
539
+ // Already have an active session
540
+ if (activeSessionId && tabLastActiveSessionId) {
541
+ return;
542
+ }
543
+
544
+ const createOrResumeSession = async () => {
545
+ try {
546
+ // Check if we need to create a new session
547
+ if (tabLastActiveSessionId) {
548
+ // Try to resume existing session
549
+ try {
550
+ await handleResumeSession(tabLastActiveSessionId);
551
+ } catch (resumeError) {
552
+ logger.debug("Failed to resume session, creating new session", {
553
+ externalSessionId: tabLastActiveSessionId,
554
+ error: resumeError,
555
+ });
556
+ // Fall back to creating new session
557
+ await handleNewSession();
558
+ }
559
+ } else {
560
+ // No existing session, create new one
561
+ await handleNewSession();
562
+ }
563
+ } catch (error) {
564
+ logger.error("Failed to create or resume session:", error);
565
+ }
566
+ };
567
+
568
+ createOrResumeSession();
569
+ // eslint-disable-next-line react-hooks/exhaustive-deps
570
+ }, [isConnected, agent, tabLastActiveSessionId, activeSessionId]);
571
+
572
+ // Handler for prompt submission
573
+ const handlePromptSubmit = useEvent(
574
+ async (_e: KeyboardEvent | undefined, prompt: string) => {
575
+ if (!activeSessionId || !agent || isLoading) {
576
+ return;
577
+ }
578
+
579
+ logger.debug("Submitting prompt to agent", {
580
+ sessionId: activeSessionId,
581
+ });
582
+ setIsLoading(true);
583
+ setPromptValue("");
584
+
585
+ // Update session title with first message if it's still the default
586
+ if (selectedTab?.title.startsWith("New ")) {
587
+ setSessionState((prev) => updateSessionTitle(prev, prompt));
588
+ }
589
+
590
+ const filename = store.get(filenameAtom);
591
+ if (!filename) {
592
+ toast({
593
+ title: "Notebook must be named",
594
+ description: "Please name the notebook to use the agent",
595
+ variant: "danger",
596
+ });
597
+ return;
598
+ }
599
+
600
+ try {
601
+ await agent.prompt({
602
+ sessionId: activeSessionId,
603
+ prompt: [
604
+ { type: "text", text: prompt },
605
+ {
606
+ type: "resource_link",
607
+ uri: filename,
608
+ mimeType: "text/x-python",
609
+ name: filename,
610
+ },
611
+ {
612
+ type: "resource",
613
+ resource: {
614
+ uri: "marimo_rules.md",
615
+ mimeType: "text/markdown",
616
+ text: getAgentPrompt(filename),
617
+ },
618
+ },
619
+ ],
620
+ });
621
+ } catch (error) {
622
+ logger.error("Failed to send prompt", { error });
623
+ } finally {
624
+ setIsLoading(false);
625
+ }
626
+ },
627
+ );
628
+
629
+ // Handler for stopping the current operation
630
+ const handleStop = useEvent(async () => {
631
+ if (!activeSessionId || !agent) {
632
+ return;
633
+ }
634
+ await agent.cancel({ sessionId: activeSessionId });
635
+ setIsLoading(false);
636
+ });
637
+
638
+ // Handler for manual connect
639
+ const handleManualConnect = useEvent(() => {
640
+ logger.debug("Manual connect requested", {
641
+ currentStatus: connectionState.status,
642
+ });
643
+ connect();
644
+ });
645
+
646
+ // Handler for manual disconnect
647
+ const handleManualDisconnect = useEvent(() => {
648
+ logger.debug("Manual disconnect requested", {
649
+ sessionId: activeSessionId,
650
+ currentStatus: connectionState.status,
651
+ });
652
+ disconnect();
653
+ });
654
+
655
+ const hasNotifications = notifications.length > 0;
656
+ const hasActiveSessions = sessionState.sessions.length > 0;
657
+
658
+ if (!hasActiveSessions) {
659
+ return (
660
+ <EmptyState
661
+ currentAgentId={selectedTab?.agentId}
662
+ connectionState={connectionState}
663
+ onConnect={handleManualConnect}
664
+ onDisconnect={handleManualDisconnect}
665
+ />
666
+ );
667
+ }
668
+
669
+ return (
670
+ <div className="flex flex-col flex-1 overflow-hidden mo-agent-panel">
671
+ <AgentPanelHeader
672
+ connectionState={connectionState}
673
+ currentAgentId={selectedTab?.agentId}
674
+ onConnect={handleManualConnect}
675
+ onDisconnect={handleManualDisconnect}
676
+ onRestartThread={handleNewSession}
677
+ hasActiveSession={true}
678
+ shouldShowConnectionControl={wsUrl !== NO_WS_SET}
679
+ />
680
+ <SessionTabs />
681
+
682
+ <ChatContent
683
+ sessionId={activeSessionId}
684
+ hasNotifications={hasNotifications}
685
+ connectionState={connectionState}
686
+ notifications={notifications}
687
+ pendingPermission={pendingPermission}
688
+ onResolvePermission={(option) => {
689
+ logger.debug("Resolving permission request", {
690
+ sessionId: activeSessionId,
691
+ option,
692
+ });
693
+ resolvePermission(option as RequestPermissionResponse);
694
+ }}
695
+ onRetryConnection={handleManualConnect}
696
+ />
697
+
698
+ <LoadingIndicator
699
+ isLoading={isLoading}
700
+ isRequestingPermission={!!pendingPermission}
701
+ onStop={handleStop}
702
+ />
703
+
704
+ <PromptArea
705
+ isLoading={isLoading}
706
+ activeSessionId={activeSessionId}
707
+ promptValue={promptValue}
708
+ onPromptValueChange={setPromptValue}
709
+ onPromptSubmit={handlePromptSubmit}
710
+ />
711
+ </div>
712
+ );
713
+ };
714
+
715
+ export default AgentPanel;