@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.
- package/dist/{ConnectedDataExplorerComponent-DfvW3rBn.js → ConnectedDataExplorerComponent-CBeIYi8p.js} +2 -2
- package/dist/{ImageComparisonComponent-XaJshw7d.js → ImageComparisonComponent-Bk0a0xBq.js} +1 -1
- package/dist/{_baseUniq-dN9WKF9m.js → _baseUniq-utU5_Vu-.js} +1 -1
- package/dist/{any-language-editor-CpFniVi-.js → any-language-editor-PrUUh2lr.js} +1 -1
- package/dist/{architectureDiagram-W76B3OCA-Bpg85ZKv.js → architectureDiagram-W76B3OCA-D-vOp0UU.js} +4 -4
- package/dist/assets/{worker-Y-Q4G-N2.js → worker-BcG8m3h5.js} +3 -3
- package/dist/{blockDiagram-QIGZ2CNN-DS1kOHlW.js → blockDiagram-QIGZ2CNN-IG-z8q8A.js} +5 -5
- package/dist/{c4Diagram-FPNF74CW-CyRVKssw.js → c4Diagram-FPNF74CW-5AEXIX3t.js} +2 -2
- package/dist/{channel-BilGXox7.js → channel-ECVsTGGL.js} +1 -1
- package/dist/{chunk-4BX2VUAB-CZR39zCO.js → chunk-4BX2VUAB-DfJcd9e-.js} +1 -1
- package/dist/{chunk-55IACEB6-BIH-MYov.js → chunk-55IACEB6-BwT8MejR.js} +1 -1
- package/dist/{chunk-FMBD7UC4-4PZXFZE8.js → chunk-FMBD7UC4-DW7uxNR6.js} +1 -1
- package/dist/{chunk-K7UQS3LO-CEvWKznk.js → chunk-K7UQS3LO-BGn2ZPDQ.js} +4 -4
- package/dist/{chunk-QN33PNHL-D5LO5Jq_.js → chunk-QN33PNHL-BcIbOumv.js} +1 -1
- package/dist/{chunk-QZHKN3VN-6gwUonWI.js → chunk-QZHKN3VN-CMSnhk6x.js} +1 -1
- package/dist/{chunk-TVAH2DTR-3gm06QdU.js → chunk-TVAH2DTR-CZF2JRya.js} +3 -3
- package/dist/{chunk-TZMSLE5B-Cm8Iy9bO.js → chunk-TZMSLE5B-BHzN_BY6.js} +1 -1
- package/dist/{classDiagram-v2-RKCZMP56-DC529O_z.js → classDiagram-KNZD7YFC-2H7MseyB.js} +2 -2
- package/dist/{classDiagram-KNZD7YFC-DC529O_z.js → classDiagram-v2-RKCZMP56-2H7MseyB.js} +2 -2
- package/dist/{clone-CLoRX3j6.js → clone-DKQcSK7N.js} +1 -1
- package/dist/{cose-bilkent-S5V4N54A-qf5DlS6Y.js → cose-bilkent-S5V4N54A-CgvKFxTr.js} +2 -2
- package/dist/{dagre-5GWH7T2D-Ceocls0m.js → dagre-5GWH7T2D-VNFIipzt.js} +6 -6
- package/dist/{data-grid-overlay-editor-AqDS_UKe.js → data-grid-overlay-editor-XdqkKCVx.js} +2 -2
- package/dist/{diagram-N5W7TBWH-CP66oSiv.js → diagram-N5W7TBWH-D1s8h-eH.js} +5 -5
- package/dist/{diagram-QEK2KX5R-_YD4kxxi.js → diagram-QEK2KX5R-DOa-AstT.js} +3 -3
- package/dist/{diagram-S2PKOQOG-Cnj8T-OP.js → diagram-S2PKOQOG-CFZ-Y2zi.js} +3 -3
- package/dist/{dockerfile-Cm8cRYCN.js → dockerfile-zE-2DWBS.js} +1 -1
- package/dist/{erDiagram-AWTI2OKA-CGnvoHx6.js → erDiagram-AWTI2OKA-WxUYJfbS.js} +4 -4
- package/dist/{flowDiagram-PVAE7QVJ-DG-pr9R9.js → flowDiagram-PVAE7QVJ-dDZH2O1W.js} +5 -5
- package/dist/{ganttDiagram-OWAHRB6G-JmChtxvn.js → ganttDiagram-OWAHRB6G-D3CCqPQq.js} +4 -4
- package/dist/{gitGraphDiagram-NY62KEGX-D8wLfOPd.js → gitGraphDiagram-NY62KEGX-BHFylEwc.js} +4 -4
- package/dist/{glide-data-editor-9nC3iCIZ.js → glide-data-editor-D0aJSGV_.js} +3 -3
- package/dist/{graph-CoRe7vAN.js → graph-BPGEu6c8.js} +3 -3
- package/dist/{index-6qYeHHjQ.js → index-Bx2b23rX.js} +3 -3
- package/dist/{index-BthgsgYX.js → index-DotQhzoN.js} +1 -1
- package/dist/{index-jkm77Jrz.js → index-HtOEKQ3O.js} +1 -1
- package/dist/{index-BpzLh4Qe.js → index-eDB61tLS.js} +1 -1
- package/dist/{infoDiagram-STP46IZ2-BlXxvOrR.js → infoDiagram-STP46IZ2-DWhhqGPi.js} +2 -2
- package/dist/{journeyDiagram-BIP6EPQ6-CNRYs_Fc.js → journeyDiagram-BIP6EPQ6-CU8FpryL.js} +3 -3
- package/dist/{kanban-definition-6OIFK2YF-B9HeMAuP.js → kanban-definition-6OIFK2YF-CWhF_a4g.js} +2 -2
- package/dist/{layout-m2vOUiW1.js → layout-DGonEvAZ.js} +4 -4
- package/dist/{linear-DU6Q5CX3.js → linear-Cww2a6nQ.js} +1 -1
- package/dist/{main-BD2KGFpU.js → main-Bc0LY9fB.js} +20636 -20608
- package/dist/main.js +1 -1
- package/dist/{mermaid-HVCtvbyx.js → mermaid-DpJuOhRr.js} +30 -30
- package/dist/{min-DcGMA4e_.js → min-CFQjsG4L.js} +2 -2
- package/dist/{mindmap-definition-Q6HEUPPD-BW8UmIDQ.js → mindmap-definition-Q6HEUPPD-K513Ef1t.js} +3 -3
- package/dist/{number-overlay-editor-D8Hl0Syo.js → number-overlay-editor-DuSchUfE.js} +2 -2
- package/dist/{pieDiagram-ADFJNKIX-Bg-3zg5U.js → pieDiagram-ADFJNKIX-DAIIUJJO.js} +3 -3
- package/dist/{quadrantDiagram-LMRXKWRM-BO4IG6Yz.js → quadrantDiagram-LMRXKWRM-yuf-j7Os.js} +2 -2
- package/dist/{react-plotly-dkvHVuRb.js → react-plotly-B378DZ9U.js} +1 -1
- package/dist/{requirementDiagram-4UW4RH46-5sdTguSM.js → requirementDiagram-4UW4RH46-BBWvEl6q.js} +3 -3
- package/dist/{sankeyDiagram-GR3RE2ED-Buhlv9OI.js → sankeyDiagram-GR3RE2ED-B_TwV-dS.js} +1 -1
- package/dist/{sequenceDiagram-C3RYC4MD-C3qsM2UP.js → sequenceDiagram-C3RYC4MD-BVC6lltp.js} +3 -3
- package/dist/{slides-component-D209A0-s.js → slides-component-CPX3S0Y9.js} +1 -1
- package/dist/{stateDiagram-KXAO66HF-CopJ7G6P.js → stateDiagram-KXAO66HF-BCU1tYTD.js} +4 -4
- package/dist/{stateDiagram-v2-UMBNRL4Z-CejL8AKf.js → stateDiagram-v2-UMBNRL4Z-BdvN6wTu.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{time-BwSBitlN.js → time-CSIip6fV.js} +2 -2
- package/dist/{timeline-definition-XQNQX7LJ-DzMNTX-C.js → timeline-definition-XQNQX7LJ-CCxCPNQI.js} +1 -1
- package/dist/{treemap-75Q7IDZK-zeJG07dk.js → treemap-75Q7IDZK-Du6v0BzD.js} +5 -5
- package/dist/{vega-component-CUkiTayd.js → vega-component-Da93sTnp.js} +2 -2
- package/dist/{xychartDiagram-6GGTOJPD-DiENNXMS.js → xychartDiagram-6GGTOJPD-Oq6xaZKR.js} +2 -2
- package/package.json +6 -3
- package/src/components/ai/ai-provider-icon.tsx +5 -1
- package/src/components/chat/acp/__tests__/__snapshots__/prompt.test.ts.snap +304 -0
- package/src/components/chat/acp/__tests__/atoms.test.ts +56 -0
- package/src/components/chat/acp/__tests__/prompt.test.ts +12 -0
- package/src/components/chat/acp/__tests__/state.test.ts +621 -0
- package/src/components/chat/acp/agent-docs.tsx +78 -0
- package/src/components/chat/acp/agent-panel.css +23 -0
- package/src/components/chat/acp/agent-panel.tsx +715 -0
- package/src/components/chat/acp/agent-selector.tsx +138 -0
- package/src/components/chat/acp/blocks.tsx +664 -0
- package/src/components/chat/acp/common.tsx +198 -0
- package/src/components/chat/acp/prompt.ts +284 -0
- package/src/components/chat/acp/scroll-to-bottom-button.tsx +50 -0
- package/src/components/chat/acp/session-tabs.tsx +138 -0
- package/src/components/chat/acp/state.ts +263 -0
- package/src/components/chat/acp/thread.tsx +121 -0
- package/src/components/chat/acp/types.ts +63 -0
- package/src/components/chat/acp/utils.ts +45 -0
- package/src/components/chat/tool-call-accordion.tsx +1 -1
- package/src/components/editor/chrome/types.ts +10 -0
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +17 -3
- package/src/core/config/feature-flag.tsx +2 -0
- package/src/plugins/impl/vega/vega.css +121 -0
- 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;
|